java多线程,有这一篇就够了

856 阅读9分钟

这是我参与8月更文挑战的第五天,活动详情查看:8月更文挑战

创建线程的三种方式

  1. 继承Thread

     class Demo2 extends Thread {
         @Override
         public synchronized void start() {
             System.out.println("通过继承Thread创建一个线程");
         }
     }
    
  2. 实现Runable接口

     class Demo1 implements Runnable {
         @Override
         public void run() {
             System.out.println("通过继承Thread创建一个线程");
         }
    
  3. 实现Callable接口

     public class Demo implements Callable {
         @Override
         public Object call() throws Exception {
             return "test";
         }
     }
    
  1. 通过线程池创建

    下面会详讲!

前三种方式比较:

  • Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
  • Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
  • Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
  • 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
  • Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

线程的状态

  1. 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
  2. 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
  3. 运行(running)状态: 执行run()方法
  4. 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
  5. 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

多线程的方法

start() 与 run()

start():是开启一个线程,申请CPU进行调度,线程就进入了就绪状态。

run():是线程中的一个方法,调用这个方法就和调用普通方法一样,在main线程中调用了这个方法而已。

sleep() 与 interrupt()

sleep():进入睡眠状态,睡眠的时间可以随机指定,睡眠过程中会让出CPU的使用权交给其他线程;但其睡眠过程中保持着锁,所以苏醒过后能立即进入工作状态。

interrupt():唤醒睡眠中的线程,该方法会使sleep()方法抛出InterruptedException异常,线程有异常抛出就会中断,但是只要你处理掉这个异常就不会中断,从而让程序苏醒。

 public class MyThread extends Thread{
     @Override
     public void run() {
         System.out.println("我要睡觉了");
         try {
             Thread.sleep(100000000);
         } catch (InterruptedException e) {
             System.out.println("为什么不让睡了");
         }
         System.out.println("醒了");
     }
 }
 class Test {
     public static void main(String[] args) throws InterruptedException {
         MyThread mt = new MyThread();
         mt.start();
         for (int i = 0; i < 100; i++) {
             System.out.println(i);
         }
         mt.interrupt();
         mt.join();
         System.out.println("哈哈哈哈");
     }
 }
wait() 与 notify()

wait(): 线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。睡眠过程中会释放锁。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis) notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态 notifyAll(): 唤醒所有的wait对象

注意:wait()方法有虚假唤醒现象,在哪里睡就在哪里起;

 class Share {
     private  int number = 0;
     public synchronized void decr() throws InterruptedException {
         if(number != 1) {
             wait();
         }
             number --;
             System.out.println("test1" + Thread.currentThread().getName());
             notifyAll();
 ​
     }
     public synchronized void incr() throws InterruptedException {
         if(number !=  0) {
             wait(); //wait()方法有虚假唤醒问题,在哪里睡就在哪里醒,当被其他线程唤醒的时候,就会在这个地方执行,所以应该和循环使用,
         }else {
             number ++;
             System.out.println("test" + Thread.currentThread().getName());
             notifyAll();
         }
     }
 }
 ​
 public class ThreadDemo1 {
     
     static  Share s = new Share();
     public static void main(String[] args) {
         new ReentrantLock()
         new Thread(()->{
             for(int i = 0; i< 20; i++) {
                 try {
                     s.incr();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         },"aa").start();
         new Thread(()->{
             for(int i = 0; i< 20; i++) {
                 try {
                     s.decr();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         },"bb").start();
 ​
     }
 }
sleep与wait的区别
  • sleep在Thread类中,wait在Object类中
  • sleep不会释放锁,wait会释放锁
  • sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知
join()

将当前线程加入到父线程中,加入后父线程就会挂起,直到加入的子线程完成后才开始执行。

 class MyThread extends Thread{
     @Override
     public void run() {
         for (int i = 0; i < 100; i++) {
             System.out.println("子线程:" + i);
         }
     }
 }
 ​
 public class Test {
     public static void main(String[] args) throws InterruptedException {
         MyThread mt = new MyThread();
         mt.start();
         for (int i = 0; i < 100; i++) {
             System.out.println("主线程:" + i);
         }
         mt.join();
         System.out.println("哈哈哈哈");
     }
 }
yield()

让出CPU,但是不会释放锁。进入就绪状态,等待其他线程先使用。同样是让出CPU而且不释放锁,他和sleep()不同的是,sleep()可以知道在什么时间醒来,而yield()确定什么时间醒来,所以通常用来控制线程的访问速度。

 public class MyThread extends  Thread{
     public MyThread(String name){
         super.setName(name);
     }
     @Override
     public void run() {
         for (int i = 0; i < 1000; i++) {
             System.out.println(super.getName() + i);
             if(i%10==0){
                 Thread.yield();
             }
         }
     }
 }
 class Test {
     public static void main(String[] args) {
         MyThread mt = new MyThread("A线程" );
         MyThread mt1 = new MyThread("B线程" );
         mt.start();
         mt1.start();
     }
 }
 ​
priority()

设置线程的优先级,数字越大优先级越高。优先级用整数表示,取值范围是1~10;对于有限级高的线程来说,尽管他优先级高,但是并不意味这他能独占 CPU,只是他占用CPU的时间比较长而已。

 public class Test {
     public static void main(String[] args) {
         MyThread mt1 = new MyThread("A线程");
         MyThread mt2 = new MyThread("B线程");
         mt1.setPriority(10);//有优先级,但不代表独占CPU
         mt2.setPriority(1);
         mt1.start();
         mt2.start();
     }
 }
  class MyThread extends Thread {
     public MyThread(String name){
         super.setName(name);
     }
     @Override
     public void run() {
         for (int i = 0; i < 100; i++) {
             System.out.println(super.getName() + "==>"+ i );
         }
     }
 }

\

线程同步

synchronized关键字

synchronized的作用域:

1.是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。

2.是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

3.synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},

synchronized的加锁范围:

如果作用于普通方法,锁的是当前实例对象;

如果作用于静态同步方法,那么锁的就是当前类的Class对象;

如果作用于同步方法块,锁的是Synchonized括号里配置的对象;

synchronized加锁原理

image-20210805211410648

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。执行到开始指令的时候,线程获取对象监视器的持有权,也就是获取锁;

如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized 优化
  1. 偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。

  1. 轻量级锁

(1)轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

(2)轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

image-20210805212625315

多线程安全问题及解决

(1) 原子性

原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

(2) 可见性

它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

(3)有序性

即程序执行的顺序按照代码的先后顺序执行。

加锁解决

上面已经说过了。。。

volatile变量解决

1.保证可见性,不保证原子性

(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;

(2)这个写会操作会导致其他线程中的volatile变量缓存无效。

2.禁止指令重排

(1)重排序操作不会对存在数据依赖关系的操作进行重排序。

(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

单例模式的双重锁必须要加volatile

因为如果不加上volatile,可能造成指令重排,在多线程的过程中,可能会造成对象的创建失败。

常见的锁

自旋锁 - spinlock

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

读写锁 - rwlock

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则只对共享资源进行写操作

这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。 悲观锁 - Pessimistic Lock

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

乐观锁 - Optimistic Lock

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

可重入锁

synchronized锁和lock锁都是可重入锁,锁的是同一个对象。

最外面的锁进入后里面的锁不会造成停止。

Lock锁体系

在Java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但在JDK1.5中新增加了ReentrantLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比synchronized更加的灵活。

lock锁的使用

 class   MyService{
     private   Lock lock=  new   ReentrantLock();
   
     public   void   testMethod() {
       lock.lock();
       for  (  int   i=  0  ;i<  5  ;i++) {
         System.out.println(  "ThreadName= "  +Thread.currentThread().getName()+(  " "  +(i+  1  )));
       }
       lock.unlock();
     }
 }

condition实现等待、通知

Condition可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

 class   MyService{
     private Lock lock=  new ReentrantLock();
     private Condition condition=lock.newCondition();
     public void await() {
       try {
         lock.lock();
         System.out.println(  "await时间为"  +System.currentTimeMillis());
         condition.await();
       } catch (InterruptedException e) {
         e.printStackTrace();
       } finally {
         lock.unlock();
         System.out.println(  "锁释放了"  );
       }
     }
   
     public void signal() {
       try {
         lock.lock();
         System.out.println(  "signal时间为"  +System.currentTimeMillis());
         condition.signal();
       }  finally {
         lock.unlock();
       }
     }
 }
 class MyThread extends Thread{
     private MyService service;
   
     public MyThread(MyService service) {
       this.service=service;
     }
     @Override
     public void run() {
       service.await();
     }
 }
 public class LockTest {
   public static void main(String[] args) throws InterruptedException {
     MyService service=new MyService();
     MyThread thread=new MyThread(service);
     thread.start();
     Thread.sleep(3000);
     service.signal();
     }
 }

Object类中的wait()方法相当于Condition类中的await()方法,Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。Object类中的notify()方法相当于Condition类中的signal()方法。Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

注意:在使用Condition方法时要先调用lock.lock()代码获得同步监视器

加锁策略:

  • tryLock():尝试加锁
  • lockInterruptibly()
  • tryLock(超时时间):在一定的超时时间内尝试加锁

实现锁的种类:

  • ReadwriteLock.(读写锁)
  • ReentrantLock(可重入锁)
  • ReentrantReadWriteLock(可重入的读写锁)
  • 以及用户可以自己实现锁(比如带自旋功能的锁)

lock锁的原理

AQS(AbstractQueueSynchronizer),即抽象的队列式同步器。

实现原理(获取锁的过程):

  • 双端队列保存线程及线程同步状态
  • 通过CAS提供设置同步状态的方法

lock锁的特点

1)提供公平锁和非公平锁: 是否按照入队的顺序设置线程的同步状态,即:多个线程申请加锁操作时,是否按照时间顺序来加锁 例如:new操作的时候传一个true就是公平锁,传一个false就是非公平锁,不传就默认为false(非公平锁)

2)AQS提供的独占式、共享式设置同步状态(独占锁、共享锁)------》本质是设置线程的同步状态

独占式:只允许一个线程获取到锁 共享式:一定数量的线程共享式获取锁 3)带Reentrant关键字的lock包下的API:可重入锁 允许多次获取同一个Lock对象的锁

死锁

相互等待造成锁不能释放。得不到想要的资源而永久暂停。

原因:

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 资源分配不当

验证死锁:

  • jps
  • jstack:jvm中自带跟踪工具

线程池

经常创建和销毁线程,对性能的影响很大。可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力

四种常见线程池

CachedThreadPool()

可缓存线程池:

  1. 线程数无限制
  2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  3. 一定程序减少频繁创建/销毁线程,减少系统开销

创建方法:

 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

FixedThreadPool()

定长线程池:

  1. 可控制线程最大并发数(同时执行的线程数)
  2. 超出的线程会在队列中等待

创建方法:

 //nThreads => 最大线程数即maximumPoolSize
 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
 ​
 //threadFactory => 创建线程的方法
 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);

ScheduledThreadPool()

定长线程池:

  1. 支持定时及周期性任务执行。

创建方法:

 //nThreads => 最大线程数即maximumPoolSize
 ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

SingleThreadExecutor()

单线程化的线程池:

  1. 有且仅有一个工作线程执行任务
  2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

创建方法:

 ExecutorService singleThreadPool = Executors.newSingleThreadPool();

怎么向线程池提交一个要执行的任务啊?

通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务

ThreadPoolExecutor的策略
  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  1. 线程数量达到了corePools,则将任务移入队列等待
  1. 队列已满,新建线程(非核心线程)执行任务
  1. 队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler抛出异常