Java多线程
一、介绍
在了解线程之前,还需要简单了解进程的概念。简单的来说就是一心多用
在生活之中,我们常常可以一心多用。我可以一边打游戏,一边放着音乐听听歌,甚至可以再泡个脚。没错,这也可以理解成我的多线程生活。
而在计算机之中,也有以上同时进行的任务,这就可以叫做多线程,例如
- 进程:比如说电脑上开着游戏,音乐等其他多款应用。这每一个应用姑且可以算作一个进程
- 线程:往往一款游戏,有伤害计算,有数据上传,图像音乐等等的步骤,这每个执行的细项也可以理解成一个线程
所以总结来看,进程是一个应用运行的过程,可以包含多个线程运行,但至少必须要有一个线程,这样才能撑得起这是个进程。
线程是cpu对某个资源的调度计算的通道,这条通道下,cpu可以执行某些任务的调度。
在java中,我们从Main方法运行,所以称其为主线程 除了主线程外,java还有一个后台线程在默默地工作着,这就是GC线程,也就是垃圾回收所处的线程
二、Java线程的实现
1)继承Thread
类
package com.banmoon.mode;
/**
* 实现多线程方式
* 1、继承类Thread
* 2、实现其run方法
* 3、创建该对象,调用start方法
*/
public class ExtendsMode{
public static void main(String[] args) {
ExtendsModeA modeA = new ExtendsModeA();
ExtendsModeB modeB = new ExtendsModeB();
modeA.start();
modeB.start();
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
}
}
class ExtendsModeA extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程A ===========");
}
}
}
class ExtendsModeB extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程B ===========");
}
}
}
启动后就会发现,原本应该最后打印的主线程,居然夹杂在线程A和线程B之中了。 这也就是说这几条线程是交替执行的,计算机实际上不能做到真正的并发,但它的线程之间的切换人为感知不出来,所以就给人一种并发的错觉。 那一条线程优先执行,这和CPU的调度有关,后续会讲到。
2)实现Runnable
接口
package com.banmoon.mode;
/**
* 实现多线程方式
* 1、实现接口Runnable
* 2、构造Thread对象,将Runnable实现对象作为参数
* 3、调用Thread对象的start方法
*/
public class RunnableMode {
public static void main(String[] args) {
Thread modeA = new Thread(new RunnableModeA());
Thread modeB = new Thread(new RunnableModeB());
modeA.start();
modeB.start();
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
}
}
class RunnableModeA implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程A ===========");
}
}
}
class RunnableModeB implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程B ===========");
}
}
}
执行后效果与上方一致,打印的信息都是穿插打印的
由于Java只支持单继承,为了使得线程实现更具有灵活性,推荐使用Runnable接口方式 此外,Runnable还有Lanmbda
的简写方式 package com.banmoon.mode; /** * 实现多线程方式 * 1、实现接口Runnable,Lambda简写方式 */ public class RunnableModeByLambda { public static void main(String[] args) { new Thread(() -> { for (int i = 0; i < 1000; i++) System.out.println("=========== 线程A ==========="); }).start(); new Thread(() -> { for (int i = 0; i < 1000; i++) System.out.println("=========== 线程B ==========="); }).start(); for (int i = 0; i < 1000; i++) System.out.println("=========== 主线程 ==========="); } }
3)实现Callable
接口
package com.banmoon.mode;
import java.util.concurrent.*;
/**
* 实现多线程方式
* 1、实现Callable接口,了解
* 2、需要定义返回值类型
* 3、创建执行服务线程池,来进行执行
*/
public class CallableMode {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
Future<String> resultA = service.submit(new CallableModeA());
Future<String> resultB = service.submit(new CallableModeB());
System.out.println("结果A:" + resultA.get());
System.out.println("结果B:" + resultB.get());
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
service.shutdown();
}
}
class CallableModeA implements Callable<String>{
@Override
public String call() throws Exception {
String str = "=========== 线程A ===========";
for (int i = 0; i < 1000; i++)
System.out.println(str);
return str;
}
}
class CallableModeB implements Callable<String>{
@Override
public String call() throws Exception {
String str = "=========== 线程B ===========";
for (int i = 0; i < 1000; i++)
System.out.println(str);
return str;
}
}
这里使用到了一个执行服务工具类Executors
,它可以创建线程池,后续会讲到
三、线程状态及方法
1)状态
其实,jdk中还有一个线程状态的枚举Thread.State
和上图有些不同,但不影响理解,只是少了个就绪的状态
2)方法
Thread方法 | 是否静态 | 说明 |
setPriority | 否 | 设置线程的优先级,优先级高的更有机会优先被CPU调度,但这个不是绝对 |
sleep | 是 | 让当前所处的线程进行休眠,可以用来模拟网络延迟,放大同步问题 |
join | 否 | 插队,等待正在运行的线程终止 |
yield | 是 | 暂停当前的线程,执行其他的线程,让CPU选择再次进行选择调度 |
1、setPriority
package com.banmoon.status;
public class ThreadPriorityMethods {
public static void main(String[] args) {
Thread threadA = new Thread(new MyThread(), "线程A");
Thread threadB = new Thread(new MyThread(), "线程B");
Thread threadC = new Thread(new MyThread(), "线程C");
threadA.setPriority(9);
threadB.setPriority(5);
threadC.setPriority(1);
threadC.start();
threadB.start();
threadA.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("============" + Thread.currentThread().getName() + "============");
}
}
}
2、sleep
package com.banmoon.status;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadSleepMethods {
public static void main(String[] args) {
new Thread(() -> {
int i = 0;
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
while (i++<10){
String dateStr = sdf.format(new Date());
System.out.println(dateStr + "============ 线程A ============");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
3、join
package com.banmoon.status;
public class ThreadJoinMethods {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
System.out.println("============= 线程A"+ i +" =============");
}
});
thread.start();
for (int i = 0; i < 1000; i++) {
if(i==100)
thread.join();
System.out.println("============= 主线程"+ i +" =============");
}
}
}
4、yield
package com.banmoon.status;
public class ThreadYieldMethods {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
if(i==200)
Thread.yield();
System.out.println("============= 线程A"+ i +" =============");
}
});
thread.start();
for (int i = 0; i < 1000; i++) {
if(i==500)
Thread.yield();
System.out.println("============= 主线程"+ i +" =============");
}
}
}
四、synchronized
关键字
1)并发数据问题
看下列代码,总共有10张票,创建3个线程,每个线程都去取票,直到票数小于0,则退出
package com.banmoon.sync;
/**
* 不安全的买票服务
*/
public class TicketServer implements Runnable{
private int ticketNum = 10;
/**
* 取票
*/
@Override
public void run() {
// 获取当前线程名
String name = Thread.currentThread().getName();
while (true){
if(ticketNum<=0)
return;
System.out.println(name + ":取到了第" + ticketNum + "张票");
// 模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum--;
}
}
public static void main(String[] args) {
TicketServer ticketServer = new TicketServer();
new Thread(ticketServer, "A").start();
new Thread(ticketServer, "B").start();
new Thread(ticketServer, "C").start();
}
}
预期将会是,10,9,8,7…直到取完票,但真实的结果是
我执行了很多遍,结果远远没有出现我预期的模样。
这是因为cpu调度线程太快了,当取票完成,但票数还没有减一的时候,其他的线程读取了没有减票前的票数,所以导致出现的问题,此类问题被称为并发问题,也称线程安全问题
2)synchronized介绍
此关键字保证了访问同个资源时出现的并发问题。
他的工作原理是对指定对象进行加锁,对此锁表示占有。导致其他线程进不来,可以查看下面示例的代码
用synchronized
对取票检票进行限制,这把锁就是ticketServer
。当A线程获取锁,进入代码执行时,其他线程必须进行等待,直到A线程完成逻辑释放锁后,CPU再重新进行调度,看谁运气好能获取到这一次的锁。
在这个等待锁的线程状态,也被称为同步阻塞状态。
package com.banmoon.sync;
public class SyncTicketServer implements Runnable{
private int ticketNum = 10;
/**
* 取票
*/
@Override
public void run() {
// 获取当前线程名
String name = Thread.currentThread().getName();
while (true){
// 注意,锁住的是this对象哦,也就是32行创建出来的ticketServer
synchronized (this){
if(ticketNum<=0)
return;
System.out.println(name + ":取到了第" + ticketNum + "张票");
// 模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum--;
}
}
}
public static void main(String[] args) {
SyncTicketServer ticketServer = new SyncTicketServer();
new Thread(ticketServer, "A").start();
new Thread(ticketServer, "B").start();
new Thread(ticketServer, "C").start();
}
}
执行结果
3)死锁
诚然,synchronized可以解决同步问题,但他的缺点需要了解
- 效率:线程处于同步阻塞中,效率上不去,但这是没有办法的
- 死锁:当代码考虑不周时,将会出现死锁问题
下列示例代码展示了死锁,有两个线程,手中持有自己的一把锁,这又想获取对方手中的锁时,两个线程相持不下,都处于同步阻塞阶段,导致出现的死锁。
package com.banmoon.sync;
public class DeadLock {
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA(), "ThreadA");
Thread threadB = new Thread(new ThreadB(), "ThreadB");
threadA.start();
threadB.start();
}
}
class ThreadA implements Runnable{
@Override
public void run() {
synchronized (ThreadA.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadB.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
}
}
}
}
class ThreadB implements Runnable{
@Override
public void run() {
synchronized (ThreadB.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadB");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadA.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
}
}
}
}
真实的情况远比上述要复杂的多,但死锁的基本概念就是如此。
4)不同的修饰位置
我们现在知道,synchronized锁住的是对象,也就是获取到了对象的锁,但处于不同的修饰位置,获取哪个对象的锁也是不一致的。
简单可以分为下面几个修饰位置
1、修饰this时
package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
* sync修饰对象时
*/
public class SyncDemo1{
private void printOne(){
synchronized (this){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void printTwo(){
synchronized (this){
String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
System.out.println(format);
}
}
public static void main(String[] args) {
SyncDemo1 syncDemo1 = new SyncDemo1();
new Thread(() -> {
syncDemo1.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo1.printTwo();
}, "线程B").start();
// System.out.println(syncDemo1);// 指的就是31行创建出来的对象
}
}
如上,因为只创建一个实例,所以他们锁定的只是this
如果不信,可以再new SyncDemo1()
,让线程B去调用这个实例对象的printTwo()
,保证你看到不同的结果
2、修饰XXX.class时
package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
* 当修饰class对象时
*/
public class SyncDemo2 {
private void printOne(){
synchronized (SyncDemo2.class){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void printTwo(){
synchronized (SyncDemo2.class){
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}
public static void main(String[] args) {
SyncDemo2 syncDemo2 = new SyncDemo2();
new Thread(() -> {
syncDemo2.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo2.printTwo();
}, "线程B").start();
// System.out.println(SyncDemo2.class);// class对象具有唯一性
}
}
这个结果与上面一致,因为锁住的都是同一个对象
3、修饰成员方法时
在这里我做了对比,判断修饰this和修饰成员方法时,锁住的对象是否是同一个
package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
/**
* 修饰普通方法,锁住的到底是什么
*/
public class SyncDemo3 {
private synchronized void printOne(){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void printTwo(){
synchronized ("Don't write that"){
// synchronized (this){// 测试锁住的是否是this
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}
public static void main(String[] args) {
SyncDemo3 syncDemo3 = new SyncDemo3();
new Thread(() -> {
syncDemo3.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo3.printTwo();
}, "线程B").start();
}
}
当23行放开进行使用,会发现线程B会被线程A卡住,说明修饰成员方法时,获取的就是当前对象的锁
4、修饰静态方法时
在这里进行了对比,判断修饰静态方法、修饰this、修饰当前class对象时,获取的是什么对象的锁
package com.banmoon.sync;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
public class SyncDemo4 {
private static synchronized void printOne(){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void printTwo(){
synchronized (this){
// synchronized (SyncDemo4.class){// 判断获取的是否是SyncDemo4.class对象
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}
public static void main(String[] args) {
SyncDemo4 syncDemo4 = new SyncDemo4();
new Thread(() -> {
syncDemo4.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo4.printTwo();
}, "线程B").start();
}
}
如果线程B马上打印,那说明获取的不是同一把锁。要是线程B被线程A卡住了,那说明确实是一把锁了
总结
- 修饰代码块时:锁住的是括号中的对象
- this:指向的是当前对象,也就是实现重写run方法的类实例化出来的对象
- XXX.class:就是这个class对象,我比较喜欢使用,因为class对象具有唯一性
- 修饰方法时:
- 成员方法:成员方法所在的类所创建出来的对象,也就是谁调用了这个方法,获取的就是谁的锁
- 静态方法:当前方法所在类的class对象,XXX.class
注意:不要修饰什么乱七八糟的对象,比如字符串对象,就像上面写的Don't write that
。有时候,自己都分不清两个是不是同一个对象,还敢乱写在代码中。
5)异常释放锁
package com.banmoon.sync;
public class SyncException {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (SyncException.class){
for (int i = 0; i < 20; i++) {
if(i==10)
i = 1/0;// 异常
System.out.println("线程A:" + i);
}
}
}).start();
Thread.sleep(3000);
synchronized (SyncException.class){
for (int i = 0; i < 10; i++) {
System.out.println("主线程");
}
}
}
}
如下运行结果,线程中出现异常时,当前持有的锁会立即释放。所以一定要准确的捕获异常,可以试试将异常捕获,保证线程的安全。
五、线程通信
在上述synchronized
的代码案例中,线程获取了锁后,都是一条路走到黑的,除了异常没捕获的那次。
线程通信,主要是线程在获取锁后,主动将锁放弃,让其他线程也来喝喝汤,cpu大哥觉得你很懂事,cpu很欣慰。
1)主要方法
由于synchronized
获取的是对象的锁,所以有关线程之间的阻塞唤醒,都来自Object
类
方法名 | 功能 |
public final void wait() | 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态需要等待其他线程唤醒 |
public final native void wait(long timeout) | 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态,一段时间后本线程自动醒来 |
public final native void notify() | 唤醒其他任意一个线程,将它从阻塞状态拉回到就绪状态 |
public final native void notifyAll() | 唤醒其他所有线程,将它们从阻塞状态拉回到就绪状态 |
2)简单示例
package com.banmoon.wait;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (Demo1.class){
for (int i = 1; i <= 10; i++) {
try {
System.out.println(StrUtil.format("线程A:{},时间:{}", i, DateUtil.now()));
if(i==5){
System.out.println("睡一会,释放锁");
Demo1.class.wait();
}
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
// 等待3秒
Thread.sleep(5000);
System.out.println("主线程:唤醒");
Demo1.class.notify();
synchronized (Demo1.class){
System.out.println("主线程:唤醒");
Demo1.class.notify();
}
}
}
执行结果,主线程唤醒后,线程A继续走他未走完的路
大家可以放开上述代码的第29,30行,运行后惊奇的发现居然出了异常
这个异常是什么原因呢,在执行wait()、notify()、notifyAll()方法时,必须要持有锁。而且唤醒还一定要持有相同对象的锁,也就是使用synchronized
获取同样的对象的锁,并使用该对象进行唤醒其实很好理解,一个持有锁的线程,怎么可能会被没有持有同样锁的线程唤醒呢
3)生产者消费者模式
通过一个中间容器,来设置该容器的最大容量作为生产者线程的结束
package com.banmoon.wait;
import cn.hutool.core.util.StrUtil;
import java.util.LinkedList;
import java.util.Queue;
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Container container = new Container();
new Thread(new Consumer(container)).start();
Thread.sleep(1000);
new Thread(new Producer(container)).start();
}
}
/**
* 生产者
*/
class Producer implements Runnable{
private Container container;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
int i = 0;
while (true){
try {
container.put(i);
System.out.println(StrUtil.format("生产者:生产了{},当前数量:{}", i, container.queue.size()));
i++;
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable{
private Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
while (true){
try {
Integer i = container.get();
System.out.println("消费者:消费了" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 容器
*/
class Container{
public Queue<Integer> queue;
public int MAX_SIZE = 10;
public Container() {
this.queue = new LinkedList<>();
}
public synchronized Integer get(){
try {
if(queue.size()==0){
notifyAll();
wait();
}
return this.queue.poll();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public synchronized void put(Integer integer){
try {
if(queue.size()>=MAX_SIZE){
notifyAll();
wait();
}
this.queue.add(integer);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4)信号灯法
设置一个标志位flag,来控制线程之间的通信状态。简单改造上面的生产消费者,使通过flag
来进行通信。
package com.banmoon.wait;
import cn.hutool.core.date.DateUtil;
public class Demo3 {
public static void main(String[] args) {
Demo3Flag flag = new Demo3Flag();
new Thread(new Demo3Producer(flag)).start();
new Thread(new Demo3Consumer(flag)).start();
}
}
class Demo3Producer implements Runnable{
private Demo3Flag flag;
public Demo3Producer(Demo3Flag flag) {
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 100; i++){
flag.production();
}
}
}
class Demo3Consumer implements Runnable{
private Demo3Flag flag;
public Demo3Consumer(Demo3Flag flag) {
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 100; i++){
flag.consumption();
}
}
}
class Demo3Flag{
// 标志位 true:生成,false:消费
private boolean flag;
public Demo3Flag() {
this.flag = true;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public synchronized void production(){
if(!this.flag){
try {
wait();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":正在生产..." + DateUtil.now());
// 生产完成,标志位设置为可以消费
this.flag = false;
notifyAll();
}
public synchronized void consumption(){
if(this.flag){
try {
wait();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":正在消费..." + DateUtil.now());
// 消费完成,标志位设置为可以继续生产
this.flag = true;
notifyAll();
}
}
5)虚假唤醒问题
在线程通信中,如果使用不当,将会出现虚假唤醒的问题,运行下列代码来进行查看
package com.banmoon.wait;
/**
* 虚假唤醒,问题演示
*/
public class Demo4 {
public static void main(String[] args) {
MyDemo4 myDemo4 = new MyDemo4();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.increment();
}, "线程A").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.decrement();
}, "线程B").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.increment();
}, "线程C").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.decrement();
}, "线程D").start();
}
}
class MyDemo4{
private int number = 0;
public synchronized void increment(){
try {
if(number==1)
wait();
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void decrement(){
try {
if(number==0)
wait();
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
正常预想的结果,ABCD四个线程使得number在0和1之间反复横跳,但实际上的结果,却大大出乎我所料
这个原因很好解释,因为wait
方法有个特性,在摔倒就在哪里爬起来,然后继续向前走。简单的描述下出现问题的步骤,
- A线程,number+1
- C线程,判断后进行
wait
- A线程,判断后进行
wait
- B线程,number-1,唤醒其他线程,此时A,C被唤醒
- A线程,number+1
- C线程,number+1
- 一直持续下去…
好的,到第6步就已经出现问题了,记住上面说的,在哪里摔倒就在哪里爬起来。
第二步时,C线程在判断完成后进入等待,直到第六步被CPU调度,因为判断已经完成,所以直接进入了number+1的逻辑。
像上述这种现象被称为虚假唤醒
解决虚假唤醒 既然是由于被唤醒后没有判断导致,所以我们这里只需要将if
改为while
,让线程唤醒后的第一件事就是判断条件 修改为while
后的执行结果
六、经典笔试题
思考一下如何用3条线程循环输出ABC
package com.banmoon.question;
/**
* 使用线程循环输出ABC
*/
public class Question1 {
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ABC A = new ABC("A", c, a);
ABC B = new ABC("B", a, b);
ABC C = new ABC("C", b, c);
new Thread(A).start();
Thread.sleep(10);// 保证初始ABC的启动顺序
new Thread(B).start();
Thread.sleep(10);
new Thread(C).start();
Thread.sleep(10);
}
}
class ABC implements Runnable{
private String name;
// 下一个获取的对象
private Object prev;
// 当前对象
private Object self;
public ABC(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
while (true){
// 获取上个对象的锁
synchronized (prev) {
// 获取当前对象的锁
synchronized (self) {
System.out.print(name);
// 唤醒当前对象锁,也就是下个线程的上个对象锁
self.notifyAll();
}
try {
Thread.sleep(50);
// 释放上个对象的锁
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
七、最后
多线程使用不是难点,而多线程的锁是面试的常客,synchronized
关键字锁住的对象这一知识点必须要掌握。
关于其他什么轻量级锁,总量级锁,读写锁,重入锁等等的概念,后续会出个单章来进行理解。
关于本文出现的代码示例,已提交至码云,只看文章不懂时,一定要敲代码进行理解。
我是半月,祝你幸福!