前言
JavaEE的多线程知识点,我们在 多线程(1) 中学习到了如何创建多线程、多线程的运行状态等,而本章主要讲解多线程的线程控制手段以及线程安全。
一.线程控制方法
线程控制:就是控制线程的执行速率,让某某线程先执行等。 根据生命周期,我们可以知道线程控制方法大概有:start()、sleep()、interrupt()、join()、wait()、yield()、notify()、
1.1启动线程--start()
class MyThread extends Thread { @Override public void run() { System.out.println("这里是线程运行的代码"); } } public class Text{ public static void main(String[] args) { //创建MyThread实例 MyThread t1=new MyThread(); //调用start方法启动线程 t1.start(); //t1.run(); 虽然也是执行run函数当中的操作,但是并不是启动线程,而是调用方法 } }
通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。并且不是指调用run方法就可以使用线程运行,而是单纯的执行方法run,并不能达到并行的机制,只有使用start()方法才可以使线程运行
1.2线程睡眠---sleep()方法
线程会暂停运行,并且可以设置暂停运行的时间,例如:sleep(1000),是休眠了1秒。
注:是暂停运行,而不是停止运行,而时间的设置长度更像是闹钟,时间一到,就开始运行。
1.3中断线程--interrupt() 方法
线程一旦开始运行,想让线程强制停止,目前有2种方法。
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
1.通过共享的标记来进行沟通:
使用自定义的变量来作为标志位.并且给标志位上并且final修饰,后期会讲解volatile 关键字,使用它也可以执行操作。
public class Text{ // 写作成员变量就不是触发变量捕获的逻辑了. 而是 "内部类访问外部类的成员" , //本身就是 ok 的~~ public static boolean isQuit = false; public static void main(String[] args) throws InterruptedException { //或者 final boolean isQuit = false; Thread t = new Thread(() -> { while (!isQuit) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); // 主线程这里执行一些其他操作之后, 要让 t 线程结束. Thread.sleep(3000); // 这个代码就是在修改前面设定的标志位. isQuit = true; System.out.println("把 t 线程终止"); } }
从上面的代码当中,我们可以发现isQuit变量用于lambda表达式,因此会有一个变量捕获的环节,而在变量捕获当中,捕获到的外部变量isquit不可以修改,因此需要使用final修饰,主线程执行完毕时,将isQuit修改为true;导致子线程强行结束。
2.调用 interrupt() 方法来通知
Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
Thread.currentThread():获取当前对象的引用,isInterrupted()方法是指提供一个标志位。
Thread.interrupted():将标志位改为true
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { // Thread.currentThread 就是 t . // 但是 lambda 表达式是在构造 t 之前就定义好的. 编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象.因此wile的括号里不可以使用t. while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { // e.printStackTrace(); break; } } }); t.start(); Thread.sleep(3000); // 把上述的标志位给设置成 true t.interrupt(); }
当线程正在sleep()休眠当中,如果使用interrupt()唤醒,则会唤醒,继续运行
1.4等待线程---join()
等待线程,是指两个线程进行时,一个线程等待另一个线程执行结束,才可以执行结束。
public static void main(String[] args) { Thread b = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("张三工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("张三工作结束了"); }); Thread a = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("李四工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } try { // 如果 b 此时还没执行完毕, b.join 就会产生阻塞的情况 b.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("李四工作结束了"); }); b.start(); a.start(); }
例如,原本李四工作比张三工作要快,可是我使用了join()方法,然后让李四等待张三结束,才能结束。方法还可以使用其他参数用于不同意思。
方法 | 作用 |
join() | 等待线程结束 |
jion(long millis) | 等待线程结束,或者等待一定时间自动结束 |
而除了这些操作方法之外,我们还有wait()、notify()方法,但是需要先学习线程安全才能更好的学习。
二.线程安全
线程安全指:多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全出现的根本原因:
- 线程是抢占式执行
- 多个线程同时对共享数据进行操作,产生数据覆盖
- 内存可见性
- 指令重排序
2.1数据不安全---数据共享
⭐不安全的演示和原因
class Counter { public int count = 0; void increase() { count++; } } public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); }
例如:以上2个线程,都执行50000次的count++,按理而言count最后的结果是100000次,可是执行之后却不足100000次。数据会存放在堆区,多个线程都可以访问。
出现的原因
count++实际上操作:
- 内存的数据加载到CPU寄存器中
- 寄存器中的数据执行++
- 返回给内存。
由于count在堆上,会被多个线程共享访问, t1和t2线程同时获取count元素,进行自增,然后将结果返回内存器当中,t1返回2,t2页返回2,将t1返回的值覆盖,因此少了一次自增
⭐不安全的处理方法
线程不安全的原因主要是多个线程会共享访问数据,导致数据的原子性被破坏,因此我们可以使用一种方法,让其他线程知晓该数据正在被其他线程使用(可见性)。
方法:加锁
当一个线程在执行操作时,对需要操作的数据或者方法进行加锁,这样其他线程使用不到了。
例如,需要操作的方法或者数据是一个房间,线程一进去时,将门锁了,只有线程一操作结束,出来时,线程二才能进去
使用关键字synchronized,加锁操作:
class Counter { public int count = 0; synchronize void increase() { count++; } } public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); }
将increase()方法进行加锁,当一个线程进入时,就会进行加锁,其他线程只能等待该线程释放。
⭐synchronized的使用
synchronized关键字进行加锁时,是需要对相同的对象展开。
加锁的目的:为了互斥使用资源,而加锁的对象也是需要相同的,两个线程对同一个对象进行加锁时,会产生锁竞争/锁冲突。
class Counter { public int count = 0; void increase() { synchronized(this){ count++; } } }
this是目前对象,而我们也可以使用其他对象。
2.2数据不安全---内存可见性
⭐不安全的演示和原因
static class Counter { public int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(() -> { while (counter.flag == 0) { //代码操作 } System.out.println("循环结束!"); }); Thread t2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("输入一个整数:"); counter.flag = scanner.nextInt(); }); t1.start(); t2.start(); }
在这个代码中,我们只需要输入非零数,按逻辑而言,线程t1也应该会结束,可事实上,t1不会结束,原因:t1读取自己工作内存中的内容,而t2对flag进行修改,t1却无法得知他的修改。
问题产生的原因:
- counter.flag==0;实际上的指令:load(读指令)jcmp(比较指令)
- 编译器发现这个这个逻辑是一样的,每一次都是需要读取指令,结果却是一样的,因此,编译器会将逻辑优化掉,直接进行jcmp指令。
- 因此即使flag发生了改变,t1也无法得知。
⭐不安全的处理方法
使用volatile关键字,使用volatile关键字修饰变量之后,编译器就会禁止优化。
static class Counter { public volatile int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(() -> { while (counter.flag == 0) { //代码操作 } System.out.println("循环结束!"); }); Thread t2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("输入一个整数:"); counter.flag = scanner.nextInt(); }); t1.start(); t2.start(); }
本质上保证修饰的变量的内存可见性,禁止编译器的优化
2.3 synchronized和volatile的区别
- volatile不保证原子性,只保证了内存可见性
- synchronized保证了原子性、也保证了内存可见性
三.认识wait()、notify()
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是实际开发中,我们希望可以协调控制多个线程的执行先后顺序。
而涉及到协调控制方法:
- wait() / wait(long timeout):让线程进入等待状态
- notify() / notifyAll(): 唤醒在当前对象上等待的线程
3.1wait()方法
线程进入等待状态,如果没有唤醒,则会一直一直等待,并且需要搭配synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait()结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
代码示例:
public static void main(String[] args) throws InterruptedException { Object object = new Object(); synchronized (object) { System.out.println("等待中"); object.wait(); System.out.println("等待结束"); } }
3.2notify()方法
用于唤醒wait()进入等待的线程。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法:则是唤醒所有等待的线程。注:虽然是同时唤醒,但是需要竞争锁,因此并不是同时执行,依然有先来后到的执行
3.3wait()方法和sleep()方法的对比
理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间
wait 需要搭配 synchronized 使用. sleep 不需要.
wait 是 Object 的方法 sleep 是 Thread 的静态方法.
四.总结
volatile 能够保证内存可见性. 强制从主内存中读取数据,synchronize既可以保证原子性也可以保证内存可见性。
Java多线程实现数据共享的原理:
JVM 把内存分成了这几个区域:
方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到