一:多线程的实现

什么是线程、进程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,比如说我们打开一个QQ,QQ整个是一个进程,而QQ里面就会有多个线程,JVM中线程共有的是堆和方法区,JMM规定了所有的变量都放在主内存中,然后各个线程把主内存中的变量拷贝到自己的工作内存中,这一来一回在高并发就会产生延迟不同步。
image.png
当线程操作一个共享变量时候操作流程为:

  • 线程首先从主内存拷贝共享变量到自己的工作空间
  • 然后对工作空间里的变量进行处理
  • 处理完后更新变量值到主内存

    1:继承Thread类

  1. public class Main {
  2. public static void main(String[] args) {
  3. //创建线程传入对应的子类或实现类
  4. Thread t = new MyThread();
  5. t.start(); // 启动新线程
  6. }
  7. }
  8. //相当于自定义了一个Thread的子类
  9. class MyThread extends Thread {
  10. @Override
  11. public void run() {
  12. //在这里写入想要该线程跑的东西
  13. System.out.println("start new thread!");
  14. }
  15. }

好处:确实是实现了多线程

缺点:自定义的类只能继承一个父类,如果需要继承其他类就无法完成了

2:实现Runnable接口

  1. //线程1
  2. public class Task1 implements Runnable {
  3. @Override
  4. public void run() {
  5. System.out.println("开始处理线程1");
  6. try {
  7. Thread.sleep(100);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("我的线程标识是1"+this.toString());
  12. }
  13. }
  1. //线程2
  2. public class Task2 implements Runnable {
  3. @Override
  4. public void run() {
  5. System.out.println("开始处理线程2");
  6. try {
  7. Thread.sleep(100);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("我的线程标识是2"+this.toString());
  12. }
  13. }
  1. public class Main {
  2. public static void main(String[] args) {
  3. //创建线程传入对应的子类或实现类
  4. Thread t1 = new Task1();
  5. t1.start(); // 启动新线程
  6. Thread t2 = new Task2();
  7. t2.start(); // 启动新线程
  8. }
  9. }

优点:解决了Thread只能单继承的问题

缺点:没有返回值

3:实现Callable接口

  1. //线程1
  2. public class Task1 implements Callable {
  3. @Override
  4. public void run() {
  5. System.out.println("开始处理线程1");
  6. try {
  7. Thread.sleep(100);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("我的线程标识是1"+this.toString());
  12. //此处随便return 举个例子
  13. return 1;
  14. }
  15. }

优点:解决了单继承 并且有返回值

缺点:每次都要自己去开启、关闭线程,太麻烦了

4: 线程池

  1. public class PoolTest {
  2. public static void main(String[] args) {
  3. // 创建一个固定大小的线程池:
  4. ExecutorService es = Executors.newFixedThreadPool(4);
  5. for (int i = 0; i < 6; i++) {
  6. es.submit(new Task("" + i));
  7. }
  8. // 关闭线程池:
  9. es.shutdown();
  10. }
  11. }
  12. //线程任务实现类
  13. class Task1 implements Runnable {
  14. private final String name;
  15. public Task(String name) {
  16. this.name = name;
  17. }
  18. @Override
  19. public void run() {
  20. System.out.println("start task " + name);
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. }
  25. System.out.println("end task " + name);
  26. }
  27. }

线程池本质上也是由Runnable和Callable实现的,所以也可以说是三种,线程池只是方便管理线程。

优点

1、重用存在的线程,减少对象创建销毁的开销。
2、可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞
争,避免堵塞。
3、提供定时执行、定期执行、单线程、并发数控制等功能。

Executors

阿里编码手册不建议使用

FixedThreadPool

  • 用于负载较重的服务器,限制当前线程数量
  • 固定的核心线程数,然后任务加入阻塞队列,一直排队
  • 执行完的任务线程然后反复去获取任务再执行
  • 会因为排队太多OOM

    CachedThreadPool

  • 没有核心线程数

  • 如果有空闲线程就执行,没有就新建线程
  • 空闲线程有60秒的生存时间,然后被回收
  • 可能会因为线程太多OOM

    ScheduledThreadPool

  • 按固定周期反复执行线程任务

  • fixed-rate 按固定时间段执行 不论成功没成功
  • fixed-delay 执行成功后固定时间再执行一次
  • 可能会因为线程太多OOM

    SingleThreadExecutor

  • 串行执行任务的场景,每个任务必须按顺序执行

  • 线程池里只会有一个线程
  • 任务加入阻塞队列 不停的加
  • 会因为排队太多OOM

    提交任务

  • executor() 不需要返回值的任务

  • submit() 需要返回值的任务

ThreadPoolExecutor

  1. public class ThreadExecutor1 {
  2. //自己写的任务
  3. CallableThread callableThread=new CallableThread();
  4. public void test1(){
  5. ExecutorService executor =new ThreadPoolExecutor(5,20,60,
  6. TimeUnit.MILLISECONDS,//保活的时间单位
  7. new SynchronousQueue(),//任务队列
  8. Executors.defaultThreadFactory(),//线程工厂
  9. new ThreadPoolExecutor.AbortPolicy());//拒绝策略
  10. executor.submit(callableThread);
  11. }
  12. }
  1. [**核心配置**](<br />)
  • corePoolSize:核心线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到_workQueue任务队列中去;_
  • maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的_workQueue任务队列的类型,决定线程池会开辟的最大线程数量;_
  • keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁,保活时间
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
  • threadFactory:线程工厂,用于创建线程,一般用默认即可;
  • handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

ThreadPoolTaskExecutor(Spring)

  1. @Configuration
  2. @EnableAsync //开启多线程
  3. public class ThreadPoolConfig {
  4. @Bean("taskExecutor")
  5. public Executor asyncServiceExecutor(){
  6. ThreadPoolTaskExecutor executor =new ThreadPoolTaskExecutor();
  7. //设置核心线程数
  8. executor.setCorePoolSize(5);
  9. //最大线程数
  10. executor.setMaxPoolSize(20);
  11. //保活时间(s)
  12. executor.setKeepAliveSeconds(60);
  13. //队列大小
  14. executor.setQueueCapacity(Integer.MAX_VALUE);
  15. //线程名
  16. executor.setThreadNamePrefix("测试线程");
  17. //等待所有任务结束后 关闭线程池
  18. executor.setWaitForTasksToCompleteOnShutdown(true);
  19. //线程池初始化
  20. executor.initialize();
  21. return executor;
  22. }
  23. }
  1. @Component
  2. public class ThreadService {
  3. @Autowired
  4. TbBrandService tbBrandService;
  5. //在此操作线程池
  6. @Async("taskExecutor")
  7. public void updateBrandName(int counts){
  8. //更新品牌数量
  9. tbBrandService.updateCounts(counts);
  10. try {
  11. //延迟5秒再执行 就是让他主线程的拿数据先去执行 咱们这个更新随后 不影响他主线程的运行速度
  12. Thread.sleep(5000);
  13. System.out.println("更新完成了");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. @Async("taskExecutor")
  19. public void updateBrandCounts(int counts){
  20. }
  21. }

区别

ThreaPoolExecutor

  1. 创建线程池方式只有一种,就是走它的构造函数,参数自己指定,阿里巴巴开发手册指定**ThreadPoolExecutor**

ThreadPoolTaskExecutor

  1. ThreadPoolExecutor是一样的,只不过加入Spring的配置,在Spring中用起来方便

二:线程的状态

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
    • synchronized会导致进入Blocked状态
  • Waiting:运行中的线程,因为某些操作在等待中;
    • Sleep 会导致线程进入Waiting
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

三:线程的中断和结束

interrupt()方法

  1. public class Main {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t = new MyThread();
  4. t.start();
  5. Thread.sleep(1000);
  6. t.interrupt(); // 中断t线程
  7. t.join(); // 等待t线程结束
  8. System.out.println("end");
  9. }
  10. }
  11. class MyThread extends Thread {
  12. public void run() {
  13. Thread hello = new HelloThread();
  14. hello.start(); // 启动hello线程
  15. try {
  16. hello.join(); // 等待hello线程结束
  17. } catch (InterruptedException e) {
  18. System.out.println("interrupted!");
  19. }
  20. hello.interrupt();
  21. }
  22. }
  23. class HelloThread extends Thread {
  24. public void run() {
  25. int n = 0;
  26. while (!isInterrupted()) {
  27. n++;
  28. System.out.println(n + " hello!");
  29. try {
  30. Thread.sleep(100);
  31. } catch (InterruptedException e) {
  32. break;
  33. }
  34. }
  35. }
  36. }

如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

volatile置为false

  1. public class Main {
  2. public static void main(String[] args) throws InterruptedException {
  3. HelloThread t = new HelloThread();
  4. t.start();
  5. Thread.sleep(1);
  6. t.running = false; // 标志位置为false
  7. }
  8. }
  9. class HelloThread extends Thread {
  10. public volatile boolean running = true;
  11. public void run() {
  12. int n = 0;
  13. while (running) {
  14. n ++;
  15. System.out.println(n + " hello!");
  16. }
  17. System.out.println("end!");
  18. }
  19. }

这里主要是要将标志位 volatile 置为 false即可结束线程

四:线程安全

个人理解的线程安全就是:各个线程执行自己的操作,互相之间的操作不影响,且返回的数据都是准确无误的。

synchronized关键字(悲观锁)

悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制,解决的问题方向是 执行控制

它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

被synchronized修饰的类和对象所有操作都是原子的。
synchronized是java内置的特性,java中的一个关键字,提供了可重入的独占锁。

synchronized块是Java提供的一种强制性内置锁,每个Java对象都可以隐式的充当一个用于同步的锁的功能,这些内置的锁被称为内部锁或者叫监视器锁,执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时候会阻塞掉。拿到内部锁的线程会在正常退出同步代码块或者异常抛出后释放内部锁,这时候阻塞掉的线程才能获取内部锁进入同步代码块。

  1. public class threadTest {
  2. //默认取票数为100
  3. public static Integer tickets =100;
  4. //定义对象锁
  5. //public static final Object lock=new Object();
  6. public static final String lock="";
  7. public static void main(String[] args) {
  8. Runnable runnable =new Runnable() {
  9. @Override
  10. public void run() {
  11. //使用synchronized关键字 同步代码块
  12. synchronized (lock) {
  13. //也可以将下面的代码写成一个方法 在这里调用 方法修饰加上synchronized
  14. if (tickets > 0) {
  15. try {
  16. tickets--;
  17. Thread.sleep(20);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName() + "出票成功,已经卖出:" + (100 - tickets) + ";剩余票数" + tickets);
  22. }
  23. }
  24. }
  25. };
  26. for (int i =0;i<1000;i++){
  27. int num=i%4+1;
  28. if (num==1){
  29. Thread t1=new Thread(runnable,"售票窗口1");
  30. t1.start();
  31. }else if (num==2){
  32. Thread t2=new Thread(runnable,"售票窗口2");
  33. t2.start();
  34. }else if (num==3){
  35. Thread t3=new Thread(runnable,"售票窗口3");
  36. t3.start();
  37. }else if (num==4){
  38. Thread t4=new Thread(runnable,"售票窗口4");
  39. t4.start();
  40. }
  41. }
  42. }
  43. }

特点:

  • 确实是可以实现多线程时候的线程安全 但是使用synchronized修饰的时候 线程会获取对应的锁
  • 但是当执行该线程的时候,不能设定获得锁超时,
  • 不能中断一个正在试图获得锁的线程,其他线程只能被动等待获取锁的线程执行完然后释放锁 (或者异常后JVM自动释放) 哪怕是该线程执行 sleep 费时的io操作 也只会傻傻的等待 效率太低
  • 读操作是不需要锁的 synchronized 也会对其加锁 此外就是无法判断锁的状态

适合锁少量的代码同步问题
配合使用:

  • synchronized内部可以调用wait()使线程进入等待状态;
  • 必须在已获得的锁对象上调用wait()方法;
  • synchronized内部可以调用notify()notifyAll()唤醒其他等待线程;
  • 必须在已获得的锁对象上调用notify()notifyAll()方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。

Lock

JUC下的并发处理工具类

Reentrant Lock

适合锁大量的代码同步问题,灵活度高
可重入锁,可以中断,非公平锁
当一个线程要获取一个被其他线程占用的锁时候,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时候是否会被阻塞那?如果不需要阻塞那么我们说该锁是可重入锁,也就是说只要该线程获取了该锁,那么可以无限制次数进入被该锁锁住的代码。

  • 相对synchronized的优点:
    • 可以多线程读操作
    • 可以判断线程有没有成功获取到锁 tryLock() 等不到会结束
    • 可以手动释放锁
  • 注意点
    • lock() 必须手动释放锁 即 在finally里面放置lock.unlock()
    • 要不然会导致死锁

Condition

使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。但是,synchronized可以配合waitnotify实现线程在条件不满足时等待,条件满足时唤醒,可以使用Condition对象来实现之前waitnotify的功能。

ReadWriteLock

使用ReadWriteLock可以解决这个读不锁,写锁,它保证:

  • 只允许一个线程写入(其他线程既不能写入也不能读取);
  • 没有写入时,多个线程允许同时读(提高性能)。

StampedLock

StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

  1. public class threadTest2 {
  2. //默认取票数为100
  3. public static Integer tickets =100;
  4. Map map1=new HashMap<>();
  5. //锁 此处是ReentranLock 可重入锁
  6. public static Lock lock=new ReentrantLock();
  7. public static void main(String[] args) {
  8. Runnable runnable =new Runnable() {
  9. @Override
  10. public void run() {
  11. if (tickets > 0) {
  12. //Lock
  13. lock.lock();
  14. try {
  15. tickets--;
  16. Thread.sleep(20);
  17. System.out.println(Thread.currentThread().getClass() + "出票成功,已经卖出:" + (100 - tickets) + ";剩余票数" + tickets);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }finally {
  21. //避免出现lock 异常
  22. lock.unlock();
  23. }
  24. }
  25. }
  26. };
  27. for (int i =0;i<1000;i++){
  28. int num=i%4+1;
  29. if (num==1){
  30. Thread t1=new Thread(runnable,"售票窗口1");
  31. t1.start();
  32. }else if (num==2){
  33. Thread t2=new Thread(runnable,"售票窗口2");
  34. t2.start();
  35. }else if (num==3){
  36. Thread t3=new Thread(runnable,"售票窗口3");
  37. t3.start();
  38. }else if (num==4){
  39. Thread t4=new Thread(runnable,"售票窗口4");
  40. t4.start();
  41. }
  42. }
  43. }
  44. }

CAS (乐观锁)

CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,则交换要更新的值,也称乐观锁,属于J.U.C包,调用的Unsafe 类中方法,这是一种硬件支持的原子性操作,不能被打断或停止,无需互斥同步。

所谓乐观锁,就是假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

线程甲从主内存中拷贝了变量A为1,在自己的线程中将副本A改为了10,当线程甲准备把这个变量更新到主内存时,如果主内存A的值没有改变(期望值),还是1,那么线程甲 成功更新主内存中A的值。但如果主内存A的值已经先被其他线程改掉不为1,那么就什么都不做,这样就能保证线程安全。

也可以自己写,实现乐观锁。

ABA问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

ABA解决
互斥同步锁synchronized

如果项目只在乎数值是否正确, 那么ABA 问题不会影响程序并发的正确性。

J.U.C 包提供了一个带有时间戳的原子引用类 AtomicStampedReference 来解决该问题,它通过控制变量的版本来保证 CAS 的正确性。

  1. public class SolveCAS {
  2. // 主内存共享变量,初始值为1,版本号为1
  3. private static AtomicStampedReference<Integer> atomicStampedReference = new
  4. AtomicStampedReference<>(1, 1);
  5. public static void main(String[] args) {
  6. // t1,期望将1改为10
  7. new Thread(() -> {
  8. // 第一次拿到的时间戳
  9. int stamp = atomicStampedReference.getStamp();
  10. System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
  11. // 休眠5s,确保t2执行完ABA操作
  12. try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
  13. // t2将时间戳改为了3,cas失败
  14. boolean b = atomicStampedReference.compareAndSet(1, 10, stamp, stamp + 1);
  15. System.out.println(Thread.currentThread().getName()+" CAS是否成功:"+b);
  16. System.out.println(Thread.currentThread().getName()+" 当前最新时间戳:"+atomicStampedReference.getStamp()+" 最新值为:"+atomicStampedReference.getReference());
  17. },"t1").start();
  18. // t2进行ABA操作
  19. new Thread(() -> {
  20. // 第一次拿到的时间戳
  21. int stamp = atomicStampedReference.getStamp();
  22. System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
  23. // 休眠,修改前确保t1也拿到同样的副本,初始值为1
  24. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  25. // 将副本改为20,再写入,紧接着又改为1,写入,每次提升一个时间戳,中间t1没介入
  26. atomicStampedReference.compareAndSet(1, 20, stamp, stamp + 1);
  27. System.out.println(Thread.currentThread().getName()+" 第2次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
  28. atomicStampedReference.compareAndSet(20, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
  29. System.out.println(Thread.currentThread().getName()+" 第3次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
  30. },"t2").start();
  31. }
  32. }

Volatile 关键字

volatile,不稳定的,这个不稳定不是在给我们说不稳定,而是给JVM说,本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,解决的问题在于内存可见,即可见性,当对一个volatile变量进行写操作时,会立即把线程的工作内存中的变量刷新到主内存中。
当一个变量被声明为volatile时候,线程写入时候不会把值缓存在寄存器或者或者在其他地方,是立即刷新变量到主内存中,当线程读取的时候会从主内存去重新获取最新值,而不是使用当前线程的拷贝内存变量值,这样就能解决一定的线程或并发问题。
使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况,比如下面的代码,如果用sychronized来修饰+的操作的话,就不会这样了。
为什么说Volatile不能保证原子性?
严格意义上来说,对于单个Volatile的单纯读写具有原子性,但是比如i++这样的操作是不具有原子性的,比如2个线程同时对一个Volatile修饰的变量v进行1000次++操作,会发现结果并不是预料的2000,而会略小,这是因为可能会产生这样的情况:A线程从主内存读到v为0,然后被阻塞,B线程也读到v为0,B线程继续执行,输出1,然后A线程继续执行++操作,读出的值也是1,这里会疑问为什么可见性好像是丢失了,A线程为什么不重新读取v的值,因为++是一个复合操作,读加写3个原子性的操作,所以此处读的还是0,有人说值已经到达了寄存器,这里不确定,但是确实是不会再去读取,而是会正常执行下一句自增的操作,这个案例就可以解释为什么说Volatile不能保证原子性。

  1. public class VolatileTEST {
  2. volatile static int val = 0;
  3. public static void main(String[] args) throws InterruptedException{
  4. Thread t1 = new Thread(() -> {
  5. for(int i=0; i<300000; i++){
  6. val += 1;
  7. }
  8. });
  9. Thread t2 = new Thread(() -> {
  10. for(int i = 0; i<300000; i++){
  11. val += 1;
  12. }
  13. });
  14. long startTime = System.currentTimeMillis();
  15. t1.start();
  16. t2.start();
  17. t1.join();
  18. t2.join();
  19. System.out.println("时间:" + (System.currentTimeMillis() - startTime) + "毫秒 \
  20. System.out.println(val);
  21. }
  22. }

Volatile和sync的区别

首先,他们两个都是解决线程安全问题的,

  • 着眼点在两个不同的方面,Volatile解决的是可见性的问题,JMM要求线程去主内存中拿数据,然后在自己的私有内存中操作,然后同步给主内存,V会将这些操作都放在主内存中,保证了变量的可见性,但是不能满足对读取顺序有要求的需求
  • V仅能去修饰变量,S能修饰变量、方法、类级别的
  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile不会造成线程堵塞,sync会造成线程堵塞,即sync的开销是比较大的
  • volatile编译的变量不会被编译器优化,sync会被编译器优化,比如一个正常写一个int a=0,下面有个if的判断,a>0,return true,那肯定是恒成立,对应生成的指令就没有判断的这一条,这就是优化,如果是被volatile修饰,就不会被优化,会老老实实去判断。

    ThreadLocal

    https://www.jianshu.com/p/3c5d7f09dfbd
    ThreadLocal变量的活动范围仅为线程内,它不是用来实现共享对象的多线程安全问题的,是一个线程内部的存储类,只能在指定的线程内存储数据,数据存储以后只有在指定线程内可以得到存储的数据。
    ThreadLocal相当于是为每一个线程都创建了一个ThreadLocalMap对象,实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

不同线程的同一ThreadLocal:获取的是不同table的数组,但都是同一位置[i],通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置,以此来进行put和get
同一线程的不同ThreadLocal:获取的是相同table的不同位置[i]

ThreadLocal与Synchronized的区别

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

    五:死锁

  • 死锁四个条件
    • 互斥条件
    • 请求和保持条件
      未拿到新资源 不放弃已获得的资源
    • 不剥夺条件
    • 环路等待条件
  • 预防死锁

    • 从条件入手来看,第一个可以打破环路等待条件,线程以确定好的顺序去获得资源,线性化排序,复杂一点的可以按照银行家算法去排序
    • 第二个就是请求和保持条件打破,一次性申请所有的资源
    • 第三个就是不剥夺条件,拿不到就主动放弃,释放自己的占用的资源
  • 产生死锁,比如说在那个Reentrant Lock中,必须要在finally中写入 lock.unlock(),释放锁,要不然就会死锁。

  • 例子:定义两个静态变量A和B,然后写两个线程1、2去操作,1先Synchronized锁住A然后睡眠一会,2去锁住B然后睡眠一会,再1锁住B,2锁住A,这样的话就会产生死锁,
    1. public class DeadLock {
    2. public static String obj1 = "obj1";
    3. public static String obj3 = "obj3";
    4. public static void main(String[] args){
    5. Thread a = new Thread(new Lock1());
    6. Thread b = new Thread(new Lock2());
    7. a.start();
    8. b.start();
    9. }
    10. }
    11. class Lock1 implements Runnable{
    12. @Override
    13. public void run(){
    14. deadLock.obj2="123";
    15. try{
    16. System.out.println("Lock1 running");
    17. while(true){
    18. synchronized(DeadLock.obj1){
    19. System.out.println("Lock1 lock obj1");
    20. Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
    21. synchronized(DadLock.obj2){
    22. System.out.println("Lock1 lock obj2");
    23. }
    24. }
    25. }
    26. }catch(Exception e){
    27. e.printStackTrace();
    28. }
    29. }
    30. }
    31. class Lock2 implements Runnable{
    32. @Override
    33. public void run(){
    34. try{
    35. System.out.println("Lock2 running");
    36. while(true){
    37. synchronized(DeadLock.obj2){
    38. System.out.println("Lock2 lock obj2");
    39. Thread.sleep(3000);
    40. synchronized(DeadLock.obj1){
    41. System.out.println("Lock2 lock obj1");
    42. }
    43. }
    44. }
    45. }catch(Exception e){
    46. e.printStackTrace();
    47. }
    48. }
    49. }