Java多线程synchronized && Lock && volatile,看完这一篇就够了

一、对线程安全的理解(实际上是内存安全)

  1. 堆是共享内存,是线程不安全的
  • 当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
  • 堆是进程共有的空间,也是线程的空间,分为全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄露。
  • 注意:局部堆和全局堆都是可以共享的
  1. 栈是线程安全的
  • 栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。
  1. Java中的进程和线程分别对应什么?
  • Java中main方法启动的是一个进程也是一个主线程,main方法里面的其他线程均为子线程,是一个线程也是一个进程,一个java程序启动后它就是一个进程,进程相当于一个空盒(分配了内存空间、JVM虚拟机实例)。它只提供资源装载的空间,具体的调度并不是由进程来完成的,而是由线程来完成的。
  • 一个java程序从main开始之后,进程启动,为整个程序提供各种资源,而此时将启动一个线程,这个线程就是主线程,它将调度资源,进行具体的操作。Thread、Runnable的开启的线程是主线程下的子线程,是父子关系,此时该java程序即为多线程的,这些线程共同进行资源的调度和执行。

二、线程同步的实现方法

synchronized实现同步

1. synchronized

修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
修改一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
修改一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

2. Thread类 + synchronized修饰方法

public class MyThread extends Thread{
    
    

	private int count = 0;
	@ Override
	public synchronized void run() {
    
    
		for (int i = 0; i < 5; i++) {
    
    
			try {
    
    
				System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
			} catch (InterruptedException e) {
    
    
					e.printStackTrace();
          	}
		}
	}
}
public class MyTest
	public static void main(String[] args) {
    
    
		MyThread myThread = new MyThread();
        Thread thread1 = new Thread( myThread, "SyncThread1");
     	Thread thread2 = new Thread( myThread, "SyncThread2");
        thread1.start();
    	thread2.start();
    }
}

在这里插入图片描述
分析:

通过new MyThread()创建了一个对象myThread,这时候堆中就存在了共享资源myThread,然后对myThread对象创建两个线程,那么thread1线程和thread2线程就会共享myThreadthread1.start()thead2.start()开启了两个线程,CPU会随机调度这两个线程。假如thread1先获得synchronized锁,那么thread1先把run()执行完,然后释放锁。接着thread1获得synchronized锁,thread2把run()执行完,然后释放锁。

3. Runnable类 + synchronized修饰方法

public class MyThread implements Runnable{
    
    
	private int count = 0;
	@ Override
	public synchronized void run() {
    
    
		for (int i = 0; i < 5; i++) {
    
    
			try {
    
    
				System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
			} catch (InterruptedException e) {
    
    
					e.printStackTrace();
          	}
		}
	}
}
public class MyTest
	public static void main(String[] args) {
    
    
		MyThread myThread = new MyThread();
        Thread thread1 = new Thread( myThread, "SyncThread1");
     	Thread thread2 = new Thread( myThread, "SyncThread2");
        thread1.start();
    	thread2.start();
    }
}

在这里插入图片描述
分析:两个同步实例没什么区别,唯一改变的就是extends Threadimplements Runnable,那么两者如何选择呢?

  • 两者使用场景:多线程编程主要就是为线程编写run()方法。如何选用这两种方法?其规则是:如果编写的类必须从其他类中导出,则选出第二种方法实现多线程。因为Java不支持多重继承,继承了其他类后不能再继承Thread类,只能利用Runnable接口。
  • 两者线程同步的区别:正如上述两个例子,Runnable可以实现多个相同的程序代码的线程去共享同一个资源,而Thread并不是不可以,而是相比于Runnable来说,不太合适
  • Runnable不可以直接run:多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。

4. synchronized的经典错误

扫描二维码关注公众号,回复: 15673415 查看本文章
public class MyThread implements Runnable{
    
    
	private int count = 0;
	@ Override
	public synchronized void run() {
    
    
		for (int i = 0; i < 5; i++) {
    
    
			try {
    
    
				System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
			} catch (InterruptedException e) {
    
    
					e.printStackTrace();
          	}
		}
	}
}
public class MyTest
	public static void main(String[] args) {
    
    
        System.out.println("使用关键字synchronized每次调用进行new SyncThread()");
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        Thread thread1 = new Thread(myThread1, "SyncThread1");
        Thread thread2 = new Thread(myThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述
分析:

通过new MyThread()创建了两个对象myThread1myThread2,这时候堆中就存在了共享资源myThread1myThread2,然后对myThread1对象创建一个线程thread1,对myThread2对象创建一个线程thread2,那么thread1线程就会共享myThread1thread2线程就会共享myThread2thread1.start()thead2.start()开启了两个线程,CPU会随机调度这两个线程。那么thread1线程在执行的时候就会获取共享资源myThread1.run()的锁,thread2线程在执行的时候就会获取共享资源myThread2.run()的锁,显然两个线程共享的都不是同一个资源。

Lock实现同步

锁类型

  • 可重入锁:在执行对象中所有同步方法不用再次获得锁
  • 可中断锁:在等待获取锁过程中可中断
  • 公平锁: 先进先出
  • 不公平锁:可以插队
  • 读写锁:读的时候可以多线程一起读,写的时候必须同步地写

Lock 是接口

  • ReentrantLock实现类:可重入锁,就是说某个线程已经获得某个锁,再次获取时不会出现死锁
  • ReentrantReadWriteLock实现类:含有readLock()和writeLock()两个锁,将读写进行了分离

1. lock()方法

package lock;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class MyLockStudy implements Runnable {
    
    
 
    private int count;
    Lock l = new ReentrantLock();
 
    @Override
    public void run() {
    
    
        l.lock();
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + ": ");
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        l.unlock();
    }
 
    public static void main(String args[]) {
    
    
        MyLockStudy runn = new MyLockStudy();
        Thread thread1 = new Thread(runn, "thread1");
        Thread thread2 = new Thread(runn, "thread2");
        Thread thread3 = new Thread(runn, "thread3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在这里插入图片描述

2. tryLock()方法

package lock;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class MyLockStudy implements Runnable {
    
    
 
    private int count;
    Lock l = new ReentrantLock();
 
    @Override
    public void run() {
    
    
        if (l.tryLock()) {
    
    
            System.out.println(Thread.currentThread().getName() + "获取锁");
            for (int i = 0; i < 5; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + ": ");
                try {
    
    
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            l.unlock();
        } else {
    
    
            System.out.println(Thread.currentThread().getName() + "未获取锁");
        }
 
    }
 
    public static void main(String args[]) {
    
    
        MyLockStudy runn = new MyLockStudy();
        Thread thread1 = new Thread(runn, "thread1");
        Thread thread2 = new Thread(runn, "thread2");
        Thread thread3 = new Thread(runn, "thread3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在这里插入图片描述
3. tryLock​(long time, TimeUnit unit) 方法
在指定时间内请求获得锁,获得了就返回true,超时了获取不了则返回false

package lock;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class MyLockStudy implements Runnable {
    
    
 
    private int count;
    Lock l = new ReentrantLock();
 
    @Override
    public void run() {
    
    
        try {
    
    
            if (l.tryLock(1000, TimeUnit.MILLISECONDS)) {
    
    
                System.out.println(Thread.currentThread().getName() + "获取锁");
                for (int i = 0; i < 5; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + ": ");
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                l.unlock();
            } else {
    
    
                System.out.println(Thread.currentThread().getName() + "未获取锁");
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
 
    }
 
    public static void main(String args[]) {
    
    
        MyLockStudy runn = new MyLockStudy();
        Thread thread1 = new Thread(runn, "thread1");
        Thread thread2 = new Thread(runn, "thread2");
        Thread thread3 = new Thread(runn, "thread3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在这里插入图片描述
4. readLock().lock()和writeLock().lock()
结论:改用读写锁后 线程1和线程2 同时在读,可以感受到效率的明显提升。

public class SynchronizedDemo2 {
    
    

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
    
    
        final SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(()->{
    
    
            test.get2(Thread.currentThread());
        }).start();

        new Thread(()->{
    
    
            test.get2(Thread.currentThread());
        }).start();
    }

    public void get2(Thread thread) {
    
    
        rwl.readLock().lock();
        try {
    
    
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
    
    
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
    
    
            rwl.readLock().unlock();
        }
    }
}

对比synchronized与Lock的区别
在这里插入图片描述
synchronized不能修饰静态代码块
1. 静态代码块
在这里插入图片描述

作用:静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次。静态代码块随着类加载而加载,有多个静态代码块的,按代码块前后顺序加载。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。

应用场景:如果有些代码必须在项目启动的时候就执行,那么我们就可以使用静态代码块来实现,这种代码是主动执行的。

2. synchronized不能修饰静态代码块
我在网上没有搜索到答案,个人理解就是静态代码块在主方法之前就执行了,压根就轮不到创建多线程来执行。所以不用synchronized也罢。

此外,上述已经说了静态代码块只能执行一次,那么开启了多线程,就会执行多次静态代码块,所以synchronized不能修饰静态代码块。

volatile实现同步

大家都知道volatile的主要作用有两点:保证变量的内存可见性、禁止指令重排序

1. 保证变量的内存可见性
在理解 volatile 的内存可见性前,我们先来看看这个比较常见的多线程访问共享变量的例子。

public class VolatileExample {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread();
        myThread.start();
		
		while(myThread.isFlag()){
    
    
			System.out.println("主线程访问到 flag 变量");
		}	
}

class MyThread extends Thread {
    
    

    public static boolean flag = false;
    
    public void run() {
    
    
        flag = true;
    }
}

执行上面的程序,你会发现,控制台永远都不会输出 “主线程访问到 flag 变量” 这句话。我们可以看到,子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子。

那么,我们思考一下为什么会出现这种情况呢?这里我们就要了解一下 Java 内存模型(简称 JMM)。

Java内存模型(JMM)

  • 所有的共享变量都存储于主内存
  • 每一个线程还存在自己的工作内存,用于保留被线程使用的变量的工作副本
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。 在这里插入图片描述

然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。

正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题。

那我们要如何解决可见性问题呢?接下来我们就聊聊内存可见性以及可见性问题的解决方案。

内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。

可见性问题的解决方案
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。

这里有两种方案:synchronized加锁使用 volatile 关键字

下面我们使用这两个方案对上面的例子进行改造。

synchronized加锁
对,你没看错,就是对变量加锁,没有对run()方法加锁:因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。

public class VolatileExample {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread();
        myThread.start();
		
		synchronized (myThread){
    
    
			while(myThread.isFlag()){
    
    
				System.out.println("主线程访问到 flag 变量");
			}	
		}
}

使用volatile关键字

class MyThread extends Thread {
    
    

    public static volatile boolean flag = false;
    
    public void run() {
    
    
        flag = true;
    }
}

使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。

嗅探机制工作原理
每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。

2. 禁止指令重排序
为了性能,从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:
在这里插入图片描述
为了更好地理解重排序,请看下面的部分示例代码:

int a = 0;

// 线程 A
a = 1;           // 1
flag = true;     // 2

// 线程 B
if (flag) {
    
     // 3
  int i = a; // 4
}

单看上面的程序好像没有问题,最后 i 的值是 1。但是为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序。假设线程 A 在执行时被重排序成先执行代码 2,再执行代码 1;而线程 B 在线程 A 执行完代码 2 后,读取了 flag 变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。那么如何程序执行结果正确呢?这里仍然可以使用 volatile 关键字。

这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。

JUC的Callable实现同步

1. 三个重要类Callable、Future、Thread
FutureTask是Future的实现类

Callable需要转成 FutureTask 放进 Thread中去,是因为Callable 本身与Thread没有关系,通过FutureTask 才能和Thread产生联系。

public interface Callable<V> {
    
    
    V call() throws Exception;
}


public interface Future<V> {
    
    

    boolean cancel(boolean mayInterruptIfRunning); //尝试取消此任务的执行。

    boolean isCancelled();//如果此任务在正常完成之前被取消,则返回true 

    boolean isDone(); //如果此任务完成,则返回true 。 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true 

    V get() throws InterruptedException, ExecutionException; //获得任务计算结果

    V get(long timeout, TimeUnit unit) 
        throws InterruptedException, ExecutionException, TimeoutException;//可等待多少时间去获得任务计算结果
}

2. 使用Callable和Future

/**
 * @Author: crush
 * @Date: 2021-08-19 18:44
 * version 1.0
 */
public class CallableDemo2 {
    
    

    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
    
    
        CallableAndFutureTest callableAndFutureTest = new CallableAndFutureTest();
        FutureTask<String> task = new FutureTask<>(callableAndFutureTest);
        Thread thread1 = new Thread(task);
        thread1.start();

        System.out.println("判断任务是否已经完成::"+task.isDone());
       
        System.out.println("阻塞式获取结果::"+task.get());

        System.out.println("如果超过等待时间还未获取到结果,则会主动抛出超时常::"+task.get(2, TimeUnit.SECONDS));

    }
}

class CallableAndFutureTest implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        String str="";
        for (int i=0;i<10;i++){
    
    
            str+=String.valueOf(i);
            Thread.sleep(100);
        }
        return str;
    }
}

三、守护线程

  1. 守护线程的概念
  • 守护线程为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆
  • 守护线程类似于整个进程的一个默默无闻的小喽喽,它的生死无关重要,它却依赖整个进程而运行,哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断
  • 注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它,因为它不靠谱
  1. 守护线程的作用(举例)
  • GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终再低级别的状态中运行,用于实时监控和管理系统中的可回收资源
  1. 守护线程设置
  • 设置守护线程:thread.setDaemon(true)
  • 设置条件:thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个illegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  • 注意:在Daemon线程中产生的新线程也是Daemon线程

四、ThreadLocal原理和使用场景

  1. ThreadLocal背景
  • 每个线程都会有属于自己的本地内存,在堆中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JVM管理控制写会到主内存中。
  • 很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
  1. ThreadLocal与Synchronized的区别
    在这里插入图片描述
  2. ThreadLocal的使用
    一般ThreadLocal<?>对象存放一个变量,如果需要存放多个变量,那就需要多个ThreadLocal<?>对象
public class Test {
    
    
	//一般都会将ThreadLocal声明成一个静态字段
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();   

    public void m1(User user) {
    
    
    	threadLocal.set(user);		//设置线程本地变量的内容。
    }

    public void m2() {
    
    
    	//获取线程本地变量的内容
        User user = threadLocal.get();
        //移除线程本地变量
        threadLocal.remove();		
    }
}

举例:未使用ThreadLocal和使用ThreadLocal

public class MyDemo01{
    
    
	private String content;
	private String getContent(){
    
     return content;}
	private void setContent(String content){
    
    this.content = content;}

	public static void main(String[] args){
    
    
		MyDemo01 demo = new MyDemo01();
		for(int i = 0; i<5; i++){
    
    
			Thread thread = new Thread(new Runnable(){
    
    
				@Override
				public void run(){
    
    
					//每一个线程存一个变量,过一会儿再来取出这个变量
					demo.setContent(Thread.currentThread().getName() + "的数据");
					System.out.println("------------------");
					System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
				}
			});
		}
	}
}

在这里插入图片描述
举例:使用了ThreadLocal

public class MyDemo01{
    
    
	ThreadLocal<String> t1 = new ThreadLocal<>();
	
	private String content;
	private String getContent(){
    
     
		return t1.get();
	}
	private void setContent(String content){
    
    
		t1.set(content);
	}

	public static void main(String[] args){
    
    
		MyDemo01 demo = new MyDemo01();
		for(int i = 0; i<5; i++){
    
    
			Thread thread = new Thread(new Runnable(){
    
    
				@Override
				public void run(){
    
    
					//每一个线程存一个变量,过一会儿再来取出这个变量
					demo.setContent(Thread.currentThread().getName() + "的数据");
					System.out.println("------------------");
					System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
				}
			});
		}
	}
}

在这里插入图片描述

  1. 多个ThreadLocal怎么实现
  • 使用ThreadLocal作为key:由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子),所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。
public class Son implements Cloneable{
    
    
    public static void main(String[] args){
    
    
        Thread t = new Thread(new Runnable(){
    
      
            public void run(){
    
    
            	ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
            	threadLocal1.set(new Son());
            	System.out.println(threadLocal1.get());
            	
            	ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
            	threadLocal2.set(new Son());
            	System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}
  1. Thread、ThreadLocal、ThreadLocalMap、Entry的原理
  • 一个线程内部都有一个ThreadLocalMap
  • 一个ThreadLocalMap里面存储多个Entry
  • 一个Entry存储一个键值对,即线程本地对象ThreadLocal(key)和线程的变量副本(value)

在这里插入图片描述

  1. ThreadLocal为什么是弱引用

首先了解什么是内存泄漏:不再会被使用的对象或者变量占用的呢村不能被回收,就是内存泄漏

下图:实现是强引用,虚线是弱引用
在这里插入图片描述

  • key使用强引用:当ThreadLocalMap的key使用强引用时,无法回收堆中的ThreadLocal。因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除ThreadLocalMap强引用的key,那么堆中的ThreadLocal不会被回收,导致Entry内存泄漏
  • key使用弱引用:当ThreadLocalMap的key使用弱引用时,可以回收堆中的ThreadLocal。由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null时,在一下次ThreadLocalMap调用set()、get()、remove()方法的时候会被清楚value值的。

五、Thread类的常用方法,以及线程的状态

  1. sleep、suspend、stop、yield的区别
    在这里插入图片描述
  2. Java多线程之间的通信

synchronized的多线程通信
wait():wait()方法可以让当前线程释放对象锁并进入阻塞状态
notify():notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列
notifyAll():notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列

lock的多线程通信
await():wait()方法可以让当前线程释放对象锁并进入阻塞状态
signal():notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列
signalAll():notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列

BlockingQueue
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

  1. 说一说sleep()和wait()的区别
  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
  1. 说一说join()方法

Thread类提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

六、线程池、解释线程池参数

  1. 为什么使用线程池
  • 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。(因为线程的创建和消耗都是比较耗费资源的,所以使用线程池,将创建的线程保存在线程池中,需要使用的时候拿出来,不需要反复创建和销毁)
  • 提高响应速度:任务来了,直接从线程池中获取,而不是创建线程再执行
  • 提高线程可管理性:线程是稀缺资源(个数有限),使用线程池也可以统一分配调优监控
  1. 线程池的参数
  • corePoolSize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
  • workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进行则全部放入队列,直到整个队列被放满但任务还再持续进入则开始创建新的线程。
  • maxinumPoolSize:代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,将workQueue都占满了,还无法满足需求时,此时就会创建新的线程,但是线程池内总数不会超过最大线程总数。
  • keepAliveTime、unit:表示超过核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超过核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime来设置空闲时间。
  • Handler:任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还没执行完正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。第二种情况就是当达到最大线程数,线程池已经没有能力继续处理提交的任务时,这时候也会拒绝
  • ThreadFactory:线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂

举个栗子:设corePoolSize=5,workQueue.size=5,maxinumPoolSize = 10

  • 现在来第1、2、3、4、5个任务,corePoolSize足够,所以可以直接创建线程。
  • 再来第6、7、8、9、10个任务,corePoolSize满了,所以后面来的任务都得在workQueue排队
  • 再来第11、12、13、14、15个任务,corePoolSize和workQueue都满了,所以需要创建Queue里面排队的线程(FIFO)
  • 后面再来任务,workQueue.size和maxinumPoolSize都满了,所以使用Handler拒绝

注意:最大线程数 = maxinumPoolSize.size;最大任务数 = maxinumPoolSize.size+ workQueue.size

  1. 线程池的处理流程(按照上面例子看就行了)
    在这里插入图片描述
  2. 线程池中的阻塞队列
  • 注意:阻塞队列与workQueue并不是同一个队列
  • 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务
  • 阻塞队列可以保证任务队列中没有任务时,阻塞常驻线程(corePoolSize创建的线程,它们会一直访问一个没有任务的workQueue,消耗CPU资源),使得线程进入wait状态,释放cpu资源
  • 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存储,不至于一直占用cpu资源
  1. 为什么是先添加队列而不是先创建最大线程?
  • 在创建新线程的时候,是要获取全局锁的,这个时候其他的线程就得阻塞,影响了整体效率。
  1. 线程池中的线程复用原理
  • 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应的一个任务的限制。所以我可以认为线程池的本质就是线程复用。
  • 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对于Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
  • 核心:start()方法是创建新线程来执行run()任务,线程池就是不通过start()方法创建新线程,而是用旧现成的run()方法来执行任务。

猜你喜欢

转载自blog.csdn.net/m0_46638350/article/details/130673116