多线程与线程安全
本文总结了Java多线程编程的核心知识点,包括线程与进程的区别、线程创建方式、线程安全问题及解决方案。重点介绍了synchronized锁的四种使用方式(代码块、实例方法、静态方法、对象锁)以及Lock锁的实现,分析了线程同步机制和线程间通信的wait/notify方法。同时还探讨了线程状态转换、守护线程、中断机制、优先级调度等概念,通过生产者消费者模式等案例说明多线程编程的实际应用。文章强调合理
最近也是将多线程和线程安全这部分又回顾了一下,算是javase中的内容,这部分需要理解,以后如果学习分布式中的juc并发编程时基础必须打好,希望大家也都能不断进步,找到好工作吧。
多线程
什么是CPU
CPU 的中文名称是中央处理器,是进行逻辑运算用的,主要由运算器、控制器、寄存器三部分组成,从字面意思看就是运算器就是起着运算的作用,控制器就是负责发出 cpu 每条指令所需要的信息,寄存器就是保存运算或者指令的一些临时文件,这样可以保证更高的速度。 也就是我们的线程运行在 cpu 之上。 如果CPU很差,即使内存再大也很慢
线程和进程
进程是资源分配最小单位,线程是程序执行的最小单位。 计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。 总结:进程是资源分配最小单位,线程是程序执行的最小单位
什么是进程:
-
cpu从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程
-
一个程序如果被cpu多次被读取到内存中,则变成多个独立的进程 什么是线程: 线程是程序执行的最小单位,在一个进程中可以有多个不同的线程同时执行,每个线程可以独立负责一个模块,即使其中一个报错,也互不影响,实现并行操作。
进程中需要线程是为了 方便同一个应用程序(进程)更好并行处理/操作
比如一个应用程序需要同时监听、渲染、保存,如果只有一个线程则只能干一件事,不能同时处理,实现并行操作
使用多线程原因:
-
提高程序运行效率
-
必须使用,比如开发文本编辑器必须要求同时执行
并行/串行区别:
串行也就是单线程执行,代码执行效率非常低,代码从上向下执行;如果上方出错下方就不会运行。
并行就是多个线程(模块)并行一起执行,效率比较高。
注意: 在执行多个线程时依赖CPU进行时间片分配,实际上单核CPU并不是真正的多线程,因为一次只能执行一个线程(通过上下文切换来执行线程,每个线程的就绪和运行也会来回变),也正因如此线程并不是越多越好
使用多线程一定提高效率吗?
不一定,需要了解 cpu 调度的算法 就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务
上下文切换太频繁就会影响性能,而且一般服务器核数8核则最多开启8个线程,否则多个线程竞争CPU,频繁切换肯定效率低。
应用场景
-
客户端(移动 App 端/)开发;
-
异步发送短信/发送邮件
-
将执行比较耗时的代码改用多线程异步执行;
-
异步写入日志 日志框架底层
-
多线程下载(文件、视频等)
同步与异步的区别
同步概念:就是代码从上向下执行。 串行
异步的概念:单独分支执行 相互之间没有任何影响。 并行
例如: http请求本身是同步,如果等不到消息就会一直阻塞,所以我们可以将耗时的部分开多线程异步执行来进行优化,减少阻塞时间和吞吐量
多线程的创建方式
注意线程执行完毕就正常死亡
1)继承 Thread 类创建线程
Thread.sleep(3000)也不影响主线程,并且主线程和子线程执行顺序和快慢也是依赖CPU调度,是随机的,注意直接调用run那么就是单线程了,必须start启动
2)实现 Runnable 接口创建线程
3)使用匿名内部类的形式创建线程
这种就不用类刻意去实现Runnable,哪个方法需要多线程就写,这样更灵活一些
// 2.使用匿名内部类的形式创建线程 new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "<我是子线程>"); } }).start();
4)使用 lambda 表达式创建线程
new Thread(() -> System.out.println(Thread.currentThread().getName() + "<我是子线程>"), "我是新名字").start();
可以指定名字,不过一般默认的thread-0,1,2就足够好了
5)使用 Callable 和 Future 创建线程 (可以拿到线程的返回结果)
底层用到了juc并发包的阻塞LockSupport.park()和唤醒LockSupport.unpark()
6)使用线程池例如用 Executor 框架
底层使用到了队列
7)spring @Async 异步注解
线程安全
例子
多线程同时对同一个全局变量做写操作(只有修改才会产生,多个线程读没影响),可能会受到其他线程的干扰,就会发生线程安全性问题。
全局共享变量存储在堆内存中
注意: 当竞争CPU从就绪到运行状态然后再操作时,此时就容易发生线程安全问题,而如果俩线程一直处于运行状态会小概率发生线程安全问题
public class ThreadCount implements Runnable { private int count = 100; // 公共变量 /** * 如何保证线程一直在运行状态 死循环控制 */ @Override public void run() { while (true) { if (count > 1) { try { // 运行状态----休眠状态--cpu的执行权让给其他的线程 // 休眠----先变成就绪,然后竞争CPU变运行状态 Thread.sleep( millis: 30); // 加上这段代码才大概率发生安全问题 } catch (Exception e) { // 每次运行休眠来回换竞争肯定大概率出问题 } /** * 错误做法,可能发生线程安全问题 * count--; * System.out.println(Thread.currentThread().getName() + "," + count); */ // 正确做法 synchronized (this){ count--; System.out.println(Thread.currentThread().getName() + "," + count); } } } } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); new Thread(threadCount).start(); new Thread(threadCount).start(); } }
解决(实现同步)
注意 不同对象肯定经过synchronized才上锁,如果单纯new一个对象不经过肯定也没获取到锁
核心思想: 给可能发生线程安全的代码上锁(同一个jvm中多个线程竞争锁的资源) 分布式锁(多个jvm中竞争锁的资源)
最终只能有一个线程能够获取到锁,哪个线程能获取到,就可以执行到该代码
注意加锁的范围,如果直接 public synchronized(非公平锁) void run() 则会让多线程变成单线程(因为上述例子是进入后就死循环不释放锁) 如果没有获取锁成功 中间需要经历锁的升级过程,一直没有获取到锁就一直阻塞等待
juc并发编程: 锁有很多类型,比如: 重入锁 悲观锁 乐观锁 公平锁 非公平锁
加锁缺点: 可能影响程序执行效率
synchronized(翻译是同步)
原理: 底层jvm是用c语言写的 获取锁和释放锁都是底层虚拟机jvm实现好了,包括唤醒等,如果使用lock,我们还要手动处理好逻辑
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
synchronized(对象锁) // 可以是this,或者自己指定一个对象 { 需要保证线程安全的代码 }
注意: 只能拦截一个this对象的不同线程, 即每个对象各有一个锁
ThreadCount threadCount1 = new ThreadCount(); ThreadCount threadCount2 = new ThreadCount(); // 此时创建俩个对象,则拦截不生效,仍然出现线程安全问题(数据重复了) new Thread(threadCount1).start(); new Thread(threadCount2).start();
优化方法:
private Object objectLock = new Object(); 然后synchronized(objectLock)
private String lock = "lock"; 也ok
即可解决上述的问题
-
修饰实例方法,作用于当前实例方法加锁,进入同步代码前要获得 当前实例 的锁
-
实例方法默认是this锁(谁调就会上锁),即必须是同一个线程对象才能保证安全,此时写法比第一条写法简单了一些
public void run() { while (true) { try { //运行状态----休眠状态--cpu的执行权让给其他的线程 // 休眠----运行状态 Thread.sleep(millis: 30); } catch (Exception e) { } cal(); } } // 加在实例方法上,默认是this锁 public synchronized void call() { if (count > 1) { count--; System.out.println(Thread.currentThread().getName() + "," + count); } }
-
修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁
-
因为静态方法只有一份,所有类.class刚好也一份可以使用
public static synchronized void call() { if (count > 1) { count--; System.out.println(Thread.currentThread().getName() + "," + count); } } ----------------等价---------------- public static void call() { synchronized(ThreadCount.class){ if (count > 1) { count--; System.out.println(Thread.currentThread().getName() + "," + count); } } }
-
加锁位置一般不在死循环外加应该就没事,因为防止不释放锁变成单线程
死锁问题
例子: 一个线程先拿到自定义lock锁还需要对象锁,而另一个线程则相反,此时如果它们各拿一个,那么就会同时等另一个线程释放,结果都不放手就发生了死锁
所以尽量避免使用嵌套锁, 如果不确定可以在jdk安装的bin目录中启动jconsole.exe找到对应的模块,然后需要在启动时进行检测即可
if (count % 2 == 0) { synchronized (lock) { a(); } } else { synchronized (this) { b(); } } public synchronized void a() { System.out.println(Thread.currentThread().getName() + ",a方法..."); } public void b() { synchronized(lock){ System.out.println(Thread.currentThread().getName() + ",b方法..."); } }
面试: 线程如何实现同步? -- 如何保证线程安全性问题
使用 synchronized 锁,JDK1.6 开始 锁的升级过程 偏向锁→轻量级锁→重量锁(尽量不使用,因为效率低) 使用 Lock (JUC并发包) 锁 ,需要自己实现锁的升级过程。底层是基于 aqs + cas 实现 (依赖操作系统互斥指令,竞争锁会发生用户态到内核态切换.可能影响效率) 使用 Threadlocal,需要注意内存泄漏的问题 原子类 CAS 非阻塞式
springmvc接口注意线程安全
需要注意: Spring MVC Controller 默认是单例的 所以需要注意线程安全问题 单例的原因有二: 1、为了性能。 2、不需要多例。 @Scope (value = "prototype") 设置为多例子。
不过一般也不在方法前加,因为会影响效率,只有可能发生安全问题时再加上即可
@Scope("prototype") // 可以设置非单实例,这样我们的count就不会共享,因为每次用一个都创建新对象,所以锁也多个,然后就不阻塞了 public class CountService { private int count = 0; /** * spring 默认 bean 对象都是单例 * @return */ @RequestMapping("/count") public synchronized String count() { try { log.info(">count<" + count++); try { Thread.sleep(millis: 3000); } catch (Exception e) { } } catch (Exception e) { } return "count"; } }
等待 / 通知机制
等待/通知的相关方法是任意 Java 对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object上(方便拿取对象锁),方法如下:
-
notify ():通知一个在对象上等待的线程,使其从 main () 方法返回,而返回的前提是该线程获取到了对象的锁
-
notifyAll ():通知所有等待在该对象的线程
-
wait ():调用该方法的线程进入 WAITING 状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用 wait () 方法后,会释放对象的锁 。 (释放锁资源,同时阻塞当前线程)
-
wait()和wait(0)是一样的,表示无限等待直到被另一个线程唤醒
wait和notify必须结合synchronized锁使用,需要放在里边,所以单独出现在main中肯定也报错,没获取到锁
public class Test01 { private Object objectLock = new Object(); public static void main(String[] args) throws InterruptedException { new Test01().print(); } public void print() throws InterruptedException { synchronized (objectLock) { System.out.println(">1<"); /** * this.wait();释放锁资源 同时当前线程会阻塞 * this.wait()、notify 结合到 synchronized */ // 获取到锁的对象.wait objectLock.wait(); // 用自定义锁就要用自定义.wait,如果this锁则this(不能new Test01) 因为此时相当于重新new一个对象,而且这个对象没获取到锁就释放肯定不行 System.out.println(">2<"); // 阻塞等待后这段代码不会执行 } } }
唤醒:
注意一般不写在一起,因为让主线程唤醒,而不能自己唤醒自己
public void print() throws InterruptedException { new Thread(new Runnable(){ synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + ">1<"); try{ objectLock.wait(); }catch (Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ">2<"); }).start(); try { Thread.sleep(3000); // 主线程3s 之后唤醒该子线程 synchronized (objectLock) { objectLock.notify(); // 也是放在synchronized中才行 } } catch (Exception e) { e.printStackTrace(); } }
生产者与消费者问题
正常就是一输入一输出
个人理解:
其实就是需要一个flag标记变量,然后生产和消费都需要先获取同一把锁,然后判断flag,如果没有轮到自己就wait释放锁,否则就执行代码完毕后再通知一下另一个线程即可(因为一个线程wait之后就阻塞了,所以另一个线程通知后才能唤醒,而且唤醒写在最后边也比较合理); 如果唤醒写在前边判断中,那么因为阻塞之后就不动了,所以另一个线程执行后不符合flag也wait释放锁,但是另一个没有被唤醒,所以应该没线程执行了,死锁
Join方法
回顾: 这种线程写法都比较灵活,可以将需要的内容包装,然后在main中try-catch或者线程中也可以
private Object object = new Object(); public static void main(String[] args) throws InterruptedException { Thread01 thread01 = new Thread01(); thread01.print(); try { // 主线程 阻塞3s // 唤醒子线程 Thread.sleep(millis: 3000); thread01.notifyThread(); } catch (Exception e) { } } public void notifyThread() { synchronized (object) { object.notify(); } } public void print() throws InterruptedException { new Thread(new Runnable() { @Override public void run() { synchronized (object) { // 主动释放this 锁 同时当前主线程变成了阻塞状态 try { System.out.println(Thread.currentThread().getName() + ",1"); object.wait(); System.out.println(Thread.currentThread().getName() + ",2"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); }
join方法:
可以让线程按顺序执行,底层使用了synchronized this锁和
public class Thread02 { public static void main(String[] args) { Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1"); // 未锁 // t2需要等待t1执行完毕 Thread t2 = new Thread(new Runnable() { @Override public void run() { try { t1.join(); // 应该是t1调用了,t1 wait就相当于t2wait了,然后上边t1执行完,自动释放唤醒所有包含t1锁的部分,t2再继续执行 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",线程执行"); } }); // t3需要等待t2执行 Thread t3 = new Thread(new Runnable() { @Override public void run() { try { t2.join(); // 等待底层唤醒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",线程执行"); } }); t1.start(); t2.start(); t3.start(); } }
Join 底层原理是基于 wait 封装的,唤醒的代码在 jvm Hotspot 源码中(应该用C++写的)。当 jvm 在关闭线程之前会检测阻塞在 t1 线程对象上的线程,然后执行 notfyAll (), 这样 t2 就被唤醒
public static void main(String[] args) throws InterruptedException { Thread06 thread06 = new Thread06(); Thread thread = thread06.print(); thread.start(); try { Thread.sleep(millis: 3000); // 中断线程 thread.interrupt(); // 底层自动唤醒 线程处于wait()/sleep()/join()才可中断 } catch (Exception e) { } } public Thread print() { Thread thread = new Thread(() -> { synchronized (object) { System.out.println("1"); try { object.wait(timeout: 0); // 阻塞等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("2"); } }); return thread; }
多线程底层7种状态
线程刚开始处于新建状态(就绪状态),然后调用start方法,此时不会立即变运行(因为CPU调度也是有调度算法的)
其实超时和等待的区别就是是否传递了时间
守护线程与用户线程
java 中线程分为两种类型:用户线程和守护线程。通过 Thread.setDaemon (false) 设置为用户线程;通过 Thread.setDaemon (true) 设置为守护线程。如果不设置次属性,默认为用户线程。 1. 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如jvm中的垃圾回收线程。 2. 用户线程是独立存在的,不会因为其他用户线程退出而退出。
守护线程,可以让自己main主线程只要结束,那么子线程不管是否运行完都挂掉 也是用Thread接受一下thread.setDaemon(true);
默认是用户线程, 并行
安全停止线程
调用 stop 方法 (现在不行了) Stop: 中止线程,并且清除监控器锁的信息,但是可能导致 线程安全问题,JDK 不建议用。 Destroy: JDK 未实现该方法。 Interrupt(线程中止) Interrupt 打断正在运行或者正在阻塞的线程。 1. 如果目标线程在调用 Object class 的 wait ()、wait (long) 或 wait (long, int) 方法、join ()、join (long, int) 或 sleep (long, int) 方法时被阻塞,那么 Interrupt 会生效,该线程的中断状态将被清除,抛出 InterruptedException 异常。 2. 如果目标线程是被 I/O 或者 NIO 中的 Channel 所阻塞,同样,I/O 操作会被中断或者返回特殊异常值。达到终止线程的目的。 如果以上条件都不满足,则会设置此线程的中断状态。
不过像循环中断以后还是尽量考虑如何进行完整流程时中断,要不然线程执行一半终端容易出问题
public void run(){ while(true){ System.out.println("执行"); Thread.sleep(millis: 100000); System.out.println("未执行"); } } public static void main(String[] args) { Thread09 thread09 = new Thread09(); thread09.start(); try { Thread.sleep(millis: 3000); // 这次休眠主要是让main线程有点参与感 } catch (Exception e) { } System.out.println("<<中断子线程>,>"); // 中断 阻塞或者正在运行的线程 thread09.interrupt(); // 注意,如果子线程没有sleep此时无法终端 }
if (this.isInterrupted()) { // 加上这个判断就不用sleep了 break; } 再拓展一下 private volatile boolean isStart = true; boolen while(isStart){ } // 这样就可以手动控制了 volatile也要加,主要是保证线程可进性
Lock锁(注意要写好防止安全问题)
在 jdk1.5 后新增的 ReentrantLock 类同样可达到此效果,且在使用上比 synchronized 更加灵活 相关 API: 使用 ReentrantLock 实现同步 lock () 方法:上锁 unlock () 方法:释放锁 使用 Condition 实现等待 / 通知 类似于 wait () 和 notify () 及 notifyAll () Lock 锁底层基于 AQS 实现,需要自己封装实现自旋锁。
Synchronized --- 属于 JDK 关键字 底层属于 C++ 虚拟机底层实现 Lock 锁底层基于 AQS 实现 -- 变为重量级 Synchronized 底层原理 --- 锁的升级过程
注意: 还是推荐synchronized(自动),因为lock(手动)必须自己处理好锁的升级过程
public class Thread10 { private Lock lock = new ReentrantLock(); // 手动 public static void main(String[] args) { /* * Lock 获取锁 和释放锁 需要开发人员自己定义 */ Thread10 t1 = new Thread10(); t1.print1(); try{ Thread.sleep(1000); }catch(Exception e){} Thread10 t2 = new Thread10(); } public void print1() { new Thread(new Runnable() { @Override public void run() { try { // 获取锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "获取锁成功"); } catch (Exception e) { } finally { lock.unlock(); // 这才是正常的,因为线程执行完应该自动释放锁,要不然另一个线程无法获取,一直阻塞等待 } } }, "t1").start(); } public void print2() { new Thread(new Runnable() { @Override public void run() { try { // 获取锁 lock.lock(); System.out.println(Thread.currentThread().getName() + "获取锁成功"); } catch (Exception e) { } finally { lock.unlock(); } } }, "t2").start(); } }
线程终止(结束)应该自动释放锁,强制中断(底层也会释放锁并可以通知自己从wait到唤醒状态(如果是运行就不能被中断,而且不会唤醒其他线程)
notify的随机由 JVM 等待集的实现决定与CPU调度无关(应该只负责使线程变运行状态吧)
public class Thread11 { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); // 区分synchronized的wait等方法,而且不能单纯出现 public static void main(String[] args) { Thread11 thread11 = new Thread11(); thread11.cal(); try { Thread.sleep(millis: 3000); } catch (Exception e) {} thread11.signal(); } public void signal() { try { lock.lock(); condition.signal(); // 相当于随机唤醒一个拿锁的线程 } catch (Exception e) {} finally { lock.unlock(); } } public void cal() { new Thread(new Runnable() { @Override public void run() { try { lock.lock(); // 也是要有锁才能await,注意不要使用wait、notify等,这个是Object也就是synchronized自带的 System.out.println("1"); condition.await(); // 主动释放锁 同时当前线程变为阻塞状态 System.out.println("2"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 最终也要释放 } } }).start(); } }
多线程 yield
主动释放 cpu 执行权
-
多线程 yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程。
-
具体的实现依赖于底层操作系统的任务调度器,所以不一定能真正执行
public class Thread12 extends Thread { public Thread12(String name) { super(name); } @Override public void run() { for (int i = 0; i < 50; i++) { if (i == 30) { System.out.println( Thread.currentThread().getName() + ",释放cpu执行权" ); this.yield(); } System.out.println( Thread.currentThread().getName() + "," + i ); } } public static void main(String[] args) { new Thread12("mayikt01").start(); new Thread12("mayikt02").start(); } }
核心仍然看操作系统的调度算法
了解一下即可
public class Thread13 { public static void main(String[] args) { Thread t1 = new Thread(() -> { int count = 0; for (;;) { System.out.println(Thread.currentThread().getName() + "," + count++); } }, "t1线程:"); Thread t2 = new Thread(() -> { int count = 0; for (;;) { System.out.println(Thread.currentThread().getName() + "," + count++); } }, "t2线程:"); t1.setPriority(Thread.MIN_PRIORITY); // 此时运行count会相对小一些 t1.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); } }
Join/Wait 与 sleep 之间的区别
本质都可以让运行变等待,不过区别很大
sleep (long) 方法在睡眠时不释放对象锁 (不需要synchronized代码块,可单独存在) join (long) 方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于jvm、wait执行 Wait (long) 方法在等待的过程中释放对象锁
更多推荐
所有评论(0)