java多线程 线程进程 线程同步 锁分类 线程间通信

https://www.jianshu.com/p/484f41963f7a

Java 多线程、线程同步、线程间通信

线程 & 进程

线程是操作系统中执行代码的一条路径。

在指定的代码路径上执行一次。

线程是运行在进程中的一个实体,一个进程可以有多个线程。

进程是软件中应用程序一次运行的一个实例,一个软件中可以有多个进程。

线程和进程的不同

  1. 线程是CPU调度的基本单位,多个线程共享进程的资源,

  2. 进程是操作系统分配系统资源的基本单位,进程拥有独立的运行环境。

  3. 进程的创建开销大,线程更轻量级。

  4. 进程和线程都是并发技术的一种载体

Java 多线程实现

Java 实现多线程有多种方法。

Thread & Runnable

通过创建 Thread 对象,实现其 run() 方法,并通过 Thread.start() 方法启动线程。

Thread t = new Thread() {
    @Override
    public void run(){
        ...
    }
};
t.start();

还可以通过 Runnable 对象创建 Thread 对象,并通过 Thread.start() 方法启动线程。

Runnable r = new Runnable() {
    @Override
    public void run() {
    ...
    }
};
Thread t = new Thread(r);
t.start();

推荐的写法是:

        Runnable r = () -> System.out.println("test3");
        Thread t3 = new Thread(r);
        t3.start();

FutureTask & Callable

FutureTask + Callable 实现多线程与 Thread + Runnable 的区别是前者有返回值。

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "Done";
            }
        };

        Callable<String> callable2 = () -> {
            return "Done2";
        };

        FutureTask<String> task = new FutureTask<>(callable2);
        new Thread(task).start();
        try {
            Thread.sleep(2000);
            System.out.println(task.get());
        } catch (
                Exception e) {
        }

    }

ThreadFactory

线程工厂,通过传入 Runnable 对象创建线程对象。

        ThreadFactory f = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        };
		//lambda
        ThreadFactory f = r -> new Thread(r);

        Thread t= f.newThread(() -> System.out.println("New Thread"));
        t.start();

Excutors 线程池

Excutors 是 Java 提供的线程池工具类。

  • 创建指定大小的线程池

      public static ExecutorService newFixedThreadPool(int nThreads)
    
  • 创建无限大小的线程池,初始大小为0

    public static ExecutorService newCachedThreadPool()
    
  • 创建无限大小的线程池,初始大小指定

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    
  • 创建只有一个线程的线程池

    public static ExecutorService newSingleThreadExecutor()
    

Excutors 创建线程池的方法最终都是通过 ThreadPoolExcutor 创建出来的。

img

  • corePoolSize 线程池始终活跃的线程数。
  • maximumPoolSize 线程池最大能创建的线程数。
  • keepAliveTime 始终活跃线程之外的 线程空闲之后存活的最长时间。
  • unit 存活时间单位。
  • workQueue 当线程池工作饱和时,新来的任务存放的队列。

线程同步

多线程环境下为什么要做同步

多线程运行环境下,多个线程共享进程的资源。在多个线程访问同一个资源时,

某一个线程对资源进行写操作的过程中,其他线程对这个写了一半的资源进行了读操作,

  • 或者对这个写了一半的资源进行了写操作,则会导致数据不一致,即数据错误。

多线程同步则是对共享资源的访问进行控制,让同一时间只有一个线程可以访问资源,保证数据的一致性。

Java 多线程同步机制

Java 实现多线程同步有三种常用的方式。

synchronized
  • synchronized 可以修饰代码块、实例方法、静态方法,对应同步的粒度不一样。
  • synchronized 本质是通过控制同步代码在同一时间内只有一个线程能访问,这样就保证了同步代码涉及的资源在同一时间内只有一个线程能访问,Java 中将这个控制线程的单元是称为 monitor,monitor 其实也是一段具体的代码,实现了互斥访问和访问缓存队列。
  • 任何线程在获得 monitor 对象的第一时间,会将共享内存中的资源复制到自己的缓存中;在释放 monitor 的第一时间,会将缓存中的资源复制到共享内存中。这样下一个请求获得 monitor 的线程就可以在共享内存中取到正确的数据。
volatile
  • volatile 能保证基本类型的赋值操作和对象的引用赋值操作的同步性,但不能保证对象内容的修改同步。
  • volatile 不能解决 ++ 的原子性问题。
  • volatile 基于禁止指令重排序、写后缓存失效、写后立即刷新内存机制实现了数据修改对多线程的可见性。
  • Java 在 java.util.concurrent.atomic 包下封装了基本类型的 volatile 同步实现。
ReentrantReadWriteLock
ReentrantReadWriteLock
  • ReentrantReadWriteLock 是 Java 中封装的读写锁,通过 ReentrantReadWriteLock.readLock() 拿到读锁,ReentrantReadWriteLock.writeLock() 拿到写锁,读锁和写锁分别用于对共享资源的读同步和写同步。
  • 读写锁有配套的 lock() 和 unlock() 方法,注意在异常分支调用 unlock() 释放锁对象,否则可能出现死锁的情况。

锁的优化

锁升级

Java 中锁有四种状态,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

  • 偏向锁
    • 由于大多数情况下,多线程间锁竞争是不发生的,往往都是同一个线程多次请求同一个锁,
      • 这样每次竞争锁都会增加不必要的资源消耗,为了降低竞争锁的资源消耗,引入了偏向锁。
    • 偏向锁不会主动释放锁,当线程 A 首次访问同步代码块并获取锁对象时,
      • 会在 java 对象头和栈帧中记录偏向锁的线程 id。
      • 下一次线程 A 再次获取锁的时候,只需要比较当前线程 id 和 java 对象头中的线程 id 是否一致,
      • 如果一致,则不用使用 CAS 来加锁、解锁;如果不一致(非线程A),则会去查询记录的线程 id 对应的线程是否还存活,如果没有存活,那么锁对象被标记为无锁状态,其他线程可以获取并将其重新设置为偏向锁;如果存活,则会去该线程的栈帧信息中查询此线程是否还需要继续持有这个锁对象,如果不需要则和线程不存活处理一致;如果仍然需要,则会暂停此线程,撤销偏向锁,升级为轻量级锁。
  • 轻量级锁
    • 在大多数情况下,除了锁竞争的现象不发生,还有线程持有锁的时间一般也不长。
      • 阻塞线程需要将 CPU 从用户态切换到内核态,会消耗资源,如果阻塞不久立即被唤醒,阻塞线程带来的资源消耗就有点得不偿失,
      • 因此这种状况下,还不如让线程自旋着等待锁释放,这就是轻量级锁。
    • 当线程 A 持有的偏向锁时,线程 B 尝试竞争锁,这时候线程 B 就会自旋在那等待线程 A 释放锁,
      • 如果自旋次数达到了设定的阈值线程 A 还没有释放锁,
      • 或者是线程 B 在自旋的过程中,又有线程 C 尝试竞争这个锁对象,
      • 这个时候轻量级锁就会升级为重量级锁。
  • 重量级锁
    • 重量级锁会把除了拥有锁的线程之外的其他线程全部阻塞,防止 CPU 空旋。

锁粗化

  • 一般来说,锁的粒度应该尽可能小,这样就能缩短其他线程的阻塞时间,等待的线程能尽快竞争到锁资源。但是加锁和解锁需要额外的资源消耗,如果锁粒度过小,则会导致一系列的加锁解锁操作,可能会导致不必要的资源消耗。
  • 锁粗化就是将多个连续的加锁、解锁操作连接到一起,扩展成一个更大的锁,避免频繁的加锁和解锁操作。

锁消除

  • Java 虚拟机在 JIT 编译时,通过对上下文进行扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁。

多线程间的通信

线程间的通信指的是两个线程之间的交互,如启动、终止、等待/唤醒。

  • 启动

    启动一个线程的方法有多种,参考 Java 多线程实现章节,这里每一种实现方法都是由一个线程启动另外一个线程的方法。

  • 终止

    • Thread.stop()

      暴力停止一个线程,线程的执行会立即停止掉。

    • Thread.interrupt()

      标记线程为终止状态,需要配合Thread.interrupt() 或 isInterrupted() 使用来终止线程。

  • 等待/ 唤醒

    • wait()

      一个线程需要等待另外一个线程执行完成,调用 wait() 可以使得当前线程进入到当前同步块 monitor 对象的等待唤醒队列。

    • notify() / notifyAll()

      notify() 用于唤醒 monitor 等待唤醒队列中的一个线程;notifyAll() 用于唤醒 monitor 等待唤醒队列中的所有线程。

    • join()

      等待调用线程执行完成,再继续往下执行。

    • yeild()

      暂时释放自身资源给同优先级线程使用。

总结

读完应该理解:

  1. 线程、进程是什么,有什么特点,为什么会将这两个联系到一起。
  2. Java 实现多线程的几种方式。
  3. 多线程执行环境下会有什么问题,Java 实现同步的几种方法。
  4. 线程间通信的几种手段和原理。

前置知识

  • 上个更详细的文档

https://www.jianshu.com/p/484f41963f7a

  • 本次的文档

https://www.jianshu.com/p/ce37438b6a8f

实现多线程

  • Thread 和 Runnable
  • FutureTask 和 Callable
  • ThreadFactory
  • Excutors线程池

多线程同步机制

  • synchronized
  • volatile
  • reentrantReadWriteLock

Java并发三个特性

  • 原子性 就是放在同一个事物里

  • 可见性

    • 个线程修改了这个变量的值,其他线程能够立即看到修改的值。
  • 有序性

    • 即程序执行的顺序按照代码的先后顺序执行。
  • 事务应该具有4个属性:

    • 原子性、一致性、隔离性、持久性。
volatile不能解决原子性问题的原因,
  • 要解决原子性问题,就需要用到锁
一、轻量级锁与重量级锁
1.锁的概念

锁:一个线程对共享对象进行加锁,别的线程访问该对象时会处于等待状态,直到锁被释放,才能继续执行
补充:volatile底层也是通过lock原子性操作,但它只对写入共享变量值时进行了加锁,别的线程可能已经使用旧值副本在进行计算了、或者已经在写入了等情况,导致不同步问题

锁升级中,第一个是:偏向锁,

  • 同一个线程多次请求同一个锁,降低竞争锁的资源消耗
  • 当线程 A 首次访问同步代码块并获取锁对象时,
  • 会在 java 对象头和栈帧中记录偏向锁的线程 id。
2.轻量级锁(自旋锁)

由于我们希望cpu尽可能多的时间使用在执行代码上,而内核线程切换会消耗较多时间,所以出现了对于短时间的等待操作进行优化
自旋锁,在等待时,不会切换到内核态去切换线程,还是在当前线程继续执行,只不过执行的是一个循环,所以称之为自旋,这样做争对短时间的等待,性能会更高,造成的时间浪费也短。
**缺点:**对于长时间的等待,它一直占用着cpu资源,别的线程得不到执行

3.重量级锁

重量级锁就是切换到内核态,由OS线程调度切换到其他线程执行,当前线程进入等待队列,后面重新竞争获取锁

二、悲观锁与乐观锁

悲观锁与乐观锁是两种概念,是对线程同步的两种不同实现方式

1. 悲观锁

线程对一个共享变量进行访问,它就自动加锁,所以只能有一个线程访问它
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
**缺点:**只有一个线程对它操作时,没有必要加锁,造成了性能浪费

2.乐观锁

线程访问共享变量时不加锁,当执行完后,同步值到内存时,使用旧值和内存中的值进行判断,如果相同,那么写入,如果不相同,重新使用新值执行
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
缺点:
值相同的情况,可能被其他线程执行过
操作变量频繁时,重新执行次数多,造成性能浪费
完成比较后,写入前,被其他线程修改了值,导致不同步问题

三、Java中锁的实现
1.ReentrantLock

ReentrantLock是悲观锁,调用ReentrantLock的lock方法后,后续的代码能够一个线程执行,直到调用unlock方法

    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        //操作共享变量
        lock.unlock();
    }
2.synchronized

使用synchronized关键字,修饰方法或者代码块,实现线程同步

    public synchronized void test() {

    }

    public void test2() {
        synchronized (this) {

        }
    }

synchronized是悲观锁,JDK1.2之前,使用的是重量级锁,后续synchronized进行了优化:
1.最初没有锁,当第一个线程访问时,升级为偏向锁

**偏向锁:**如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

2.当别的线程访问时,升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作
3.自旋达到次数时,升级为重量级锁,切换内核态

在对象头中存放了锁状态

img

对象头组成.png

3.CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

JDK1.5后,新增java.util.concurrent包,上面我们知道乐观锁是有问题的,CAS是系统CPU提供的一种解决原子性问题的方案,解决了乐观锁的不同步问题

Java中AtomicXXX就是采用的CAS,它通过以下方法解决了乐观锁的问题
1.对对象增加版本号,每次操作时+1,而不是使用值进行是否重新执行的判断
2.自旋锁升级为重量级锁,防止一直自旋浪费cpu
3.调用compareAndSwapInt,通过jni调用原子性操作,保证多个线程都能够看到同一个变量的修改值

并发量不高以及耗时操作短时,使用CAS效率比悲观锁效率高

    static AtomicInteger atomicInteger = new AtomicInteger();
	//    static Integer a=1; 如果这样定义,2000,会跑丢1-2个数。结果 20000会跑丢11,12,13个数

    public static void main(String[] args) {
        for(int i = 0;i<20;i++){
            new Thread(){
                @Override
                public void run() {
                    for (int j =0;j<1000;j++){
                        System.out.println(atomicInteger.incrementAndGet());
                    }
                }
            }.start();
        }
    }
4.AQS

AQS是Java提供的同步器框架,ReentrantLock、CountDownLatch等就是使用了它,当多个线程对同一对象进行操作时,内部维护一个state和等待队列,来判断是否有锁,如果没有获取到锁,那么进入等待队列,等待其他线程操作完后进行唤醒

AbstractQueuedSynchronizer(AQS) 文摘排队同步器

public class VolatileTest4 {
    static CountDownLatch count = new CountDownLatch(20);

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread() {
                @Override
                public void run() {
                    //创建 0-1 *3000,就是 0到3000的数。0-3秒
                    long millis = (long) (new Random().nextFloat() * 3000);
                    try {
                        //睡眠
                        Thread.sleep(millis);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //进行 向下 --
                    count.countDown();
                    System.out.println(count.getCount() + "ms:" + millis);
                    super.run();
                }
            }.start();
        }

        try {
            //等待 ,线程 完毕,就 真实 减去1
            count.await();
            //最后执行 这个
            System.out.println("所有子线程执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
  • 如果只 循环10次的结果。会从小到大 睡眠的执行
19ms:327
18ms:764
17ms:1103
16ms:1203
15ms:1262
14ms:1649
13ms:1758
12ms:1804
11ms:2387
10ms:2416
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值