Java并发编程的艺术(一)

时间:2022-07-28
本文章向大家介绍Java并发编程的艺术(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程就能让程序更大限度地并发执行。--例如上下文切换的问题,死锁的问题,受限于软件和硬件的资源问题。

单核处理器也可以支持多线程编码:

CPU通过给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU通过不停地切换线程执行,让我们感受到多个线程同时执行,一般时间片的大小为几十毫秒(ms).

CPU通过时间片分配算法来循环执行任务,当一个任务执行一定的时间片后就会切换另一个任务,在切换钱会保存上一个任务的状态,一边下一切换回去的时候可以再加载这个任务的状态。所以人物从保存到再次加载的过程称为一次上下文切换。

多线程不一定比单线程快,在操作量不大的情况下,线程的创建和上下文切换反而使多线程比单线程更加慢。

减少上下文切换的方式:

1、无锁并发编程。多线程竞争锁的时候,会引起上下文切换,尽可能避免使用锁可以减少上下文的切换:如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

2、CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

3、使用最少的线程数量。大量的空闲线程(waitting状态),除了增加创建开销,还有切换上下文的开销。在任务很少的情况下尽量减少不必要的线程。

4、协程。在单线程里实现多任务的调度,并在单线程里维持多任务间的切换。

死锁

一个死锁的发现过程Demo

贴入死锁Demo代码

public class DeadLockDemo {
  private static String A = "A";
  private static String B = "B";

  public static void main(String[] args) {
    new DeadLockDemo().deadLock();
  }

  private void deadLock() {
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (A) {
          try {
            Thread.currentThread().sleep(2000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          synchronized (B) {
            System.out.println("1");
          }
        }
      }
    });
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (B) {
          synchronized (A) {
            System.out.println("2");
          }
        }
      }
    });
    t1.start();
    t2.start();
  }
}

一、获取pid

方法一:在jdk/bin目录下打开控制台,敲击jps-v找到自己的程序的Pid

方法二:在jdk/bin目录下有一个叫做jvisualvm的可执行文件,打开

二、用jstack查看日志(这里dump似乎跟我的电脑八字犯冲,stackoverflow上的方法都用了也不行,所以选择曲线救国,打印日志。)

在jdk/bin下打开cmd,输入 jstack -l 5444 > jstack.log

(jstack -l pid > 文件名.后缀)

文件太大就不放上来了,但是其中有两段引起我的注意。


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00e75444 (object 0x0f6b7850, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00e76164 (object 0x0f6b7870, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
  at com.jathonkatu.day20190716.DeadLockDemo$2.run(DeadLockDemo.java:37)
  - waiting to lock <0x0f6b7850> (a java.lang.String)
  - locked <0x0f6b7870> (a java.lang.String)
  at java.lang.Thread.run(Thread.java:745)
"Thread-0":
  at com.jathonkatu.day20190716.DeadLockDemo$1.run(DeadLockDemo.java:27)
  - waiting to lock <0x0f6b7870> (a java.lang.String)
  - locked <0x0f6b7850> (a java.lang.String)
  at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

分析一下:

线程1在等待锁0x0F6b7850,正在被锁0x0f6b7870锁住,且两者都是String类型(两个String在常量池中的地址)。

相反,线程2在等待锁0x0f6b7870,正在被锁0x0F6b7850锁住。

从上面不难得出,事实上就是一个死锁的行为,结合代码就不难分析,一个持有String对象A的锁,请求String对象B的锁,一个持有String对象B的锁,请求String对象A的锁。

避免死锁的常见方法:

1、避免一个线程同时获取多个锁

2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

3、尝试使用定时锁,ReentrantLock类中有个方法tryLock(long timeout,TimeUnit unit)来代替内部锁机制。

4、对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现失败的情况。(释放锁失败抛异常后仍然持有锁)

资源限制

在并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。

硬件资源的限制有:带宽的上传/下载速度,硬盘读写速度和CPU的处理速度。

软件资源的限制有:数据库的连接数和socket连接数等。

资源限制引发的问题

将代码串行执行的部分改成并行执行固然能加快代码运行,但如果受限于资源后,期望并行执行的代码其实还是串行执行。并且不仅仅不会加快代码执行,反而会更慢,因为增加了上下文切换和资源调度时间

如何解决资源限制问题

考虑使用集群并行执行程序,如ODPS、Hadoop或者自己搭建的服务器集群。不同的机器处理不同的数据,可以通过“数据ID%机器数”,计算计算机编号,根据不同的编号用不同的机器处理。

资源限制情况下进行并发编程

根据不同的资源限制调整程序的并发度。有数据库操作时,设计数据库连接数,如果Sql执行非常快,而线程数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库链接。如下载文件就依赖于贷款和硬盘读写速度。