当前位置: 首页 > news >正文

多线程初阶(2)

说到多线程编程,一定少不了线程安全这个话题。我们前面了解了线程的原理以及线程与进程的关系。线程之间共享资源,这就代表了在多线程编程中一定会产生冲突,所以我们需要在敲代码时保证线程安全,避免这样的问题发生。

我们先看一个代码案例

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

在我们看来,运行的结果应该是100000,但是事实并非如此。

运行结果:

1.随机调度

count++;

这段代码是看似是一个指令,但实际上是分步执行的。

分为三步的:

1.读取数据

2.修改数据

3.放回内存

在执行过程中,CPU资源是随时会被调度走的,也就是说,如果执行到了读取内存,有可能会被立刻调度走的。这就是所谓的随机调度。

为了解释上面的案例可以画一个时间线:

这也就是造成上面现象的原因之一。

那我们应该如何解决呢?

解决方案1:

利用join方法,在t1执行完后再执行t2,这样虽然能解决问题,但是失去了并发执行的意义,串行执行的效率也比较低。

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

2.锁(synchronized)

对于解决上述的线程问题,引入了锁这一概念。

锁是什么,我们可以举个具体的例子来了解一下:

我们可以把锁看成家里的锁,当家里没人时,门是从外面锁上的,这时拥有钥匙的人就可以打开锁并进去执行任务,但为了在执行任务的时候是安全的,所以会从里面锁上,这时外面的人就算有钥匙也打不开门,当里面的人执行完任务的出来后还会把门带上,这时外面拥有钥匙的人就可以开门进去了。这其实就是锁是如何解决线程安全问题的类似原理。

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

运行结果:

我们先了解一下使用锁的格式:

synchronized(锁对象){}

这里的锁对象可以是任意引用类型的对象,但要保证在解决同一个非原子问题时是同一个对象。

synchronized的使用方法除了上述格式以外,还能用来修饰方法:

这里的锁对象是this。

class Counter{int count = 0;public synchronized void addCount(){count++;}
}public class Test11 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});t1.start();t2.start();Thread.sleep(100);System.out.println(counter.count);}
}

运行结果:

其实这样的解决方法也就是将局部的任务给串行化,只不过比直接将整个线程串行化来的含蓄,性能降低的少。

可重入

在Java中synchronized具有可重入的特点。

synchronized (locker) {synchronized (locker) {count++;}}

我们知道,锁是具有互斥性的,也就是在上锁后是需要解锁后才能让下一个拥有锁对象的任务执行,那上面这段代码就会形成一个死锁的现象,当进入第一个锁后,会遇到第二个锁,但是想要进入第二个synchronized是需要从第一个synchronized中出来的,但是要想从第一个synchronized中出来就需要进入第二个synchronized,所以这就形成了一个死循环,可以叫死锁。

Java的开发人员为了解决这一问题就赋予了synchronized可重入这一属性,也就是在上述情况下不会出现死锁的现象,Java会自动识别出来。

如果有很多层synchronized嵌套的话,当第一次进入的synchronized结束时,这把锁才会解开。

死锁 

上面讲可重入性的时候讲到了死锁这一概念,这里就详细讲讲死锁。

造成死锁有三种情况:

1.一个线程同一个锁加多次,这也是讲述可重入性时举的例子。

2.N个线程,M把锁。

public class Test12 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {System.out.println("t1获取了locker1!");//确保t2先拿到locker2Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取了两把锁!");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {System.out.println("t2获取了locker2!");//确保线程t1先拿到locker1Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2获取了两把锁!");}}});t1.start();t2.start();}
}

运行结果:

进程并没有结束,使用jconsole查看线程状态,发现是BLOCKED,也就锁造成的无上限的阻塞等待。这是因为在线程t1拿到locker1和t2拿到locker2的情况下,t1要想拿到locker2就必须要让t2解锁,而t2要想解锁,就需要拿到locker1,但是locker1在t1手中,所以就形成了死循环,也就构成了死锁。

3.哲学家就餐问题

假设有七个哲学家围着一个桌子吃饭,每两个人中间放一根筷子,这样的话当一个人用筷子吃饭的时候,他两侧的人都没法用餐。

在一种极端情况下,每个哲学家都拿起他左手边的筷子,这样所有哲学家都在等待对方放下筷子,就形成了死锁。

public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){}}});Thread t3 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t4 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t5 = new Thread(()->{synchronized (locker5){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}

各线程状态: 

解决方案:

1.对筷子按顺序进行编号,先拿到左右小的编号的筷子,拿到后再拿左右大的编号的筷子。

刚开始1号先吃到饭,吃完后放下1号和7号筷子,2号拿到1号筷子,7号拿到7号筷子,7号可以就餐,以此类推,所有人都可以完成就餐。

public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{//保证t1先拿到locker1,如果t2先拿到locker1,还是会形成死锁try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t3 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t4 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});Thread t5 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}

运行结果:

 4.死锁总结:

造成死锁的原因:

1.锁具有互斥性(锁的基本特性)

当一个锁被一个线程获取之后,当别的线程想要获取这个锁的时候,会线程阻塞。

2.锁不可抢占(锁的基本特性)

当这个锁已经被获取时,别的线程是不能强行抢占这个锁的, 必须等待获取。

3.请求和保持

当一个线程已经有至少一个锁的时候,尝试获取别的锁遇到阻塞,这时候该线程也不会放弃原来的锁。

4.循环等待

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程5,线程5等待线程1,这样就产生了死循环。

解决方案:

1.把嵌套的锁改为并列的锁。(基于N个线程,M把锁的代码例子)

public class Test14 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){System.out.println("t1拿到locker1!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }}synchronized (locker2){System.out.println("t1拿到locker2!");}});Thread t2 = new Thread(()->{synchronized (locker2){System.out.println("t2拿到locker2!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }}synchronized (locker1){System.out.println("t2拿到locker1!");}});t1.start();t2.start();}
}

运行结果:

2.规定加锁顺序编号递增/递减(基于哲学家就餐问题)

代码案例在上述哲学家就餐问题中的Test13.

3.java标准库中的线程安全类:

StringBuffer,Hashtable,Vector,ConcurrentHashMap,String。。。。。。

前三个不推荐使用。

不安全类:
StringBuilder,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet。

以StringBuffer为例,查看标准库中的代码,发现其内部是由简单加synchronized实现的,当面对比较复杂的情况时,很有可能会出现bug~~

4.wait/notify

在有些情况下我们需要让某个线程处于阻塞状态,在完成某些任务后再进行唤醒。

注意在调用wait/notify时必须实在synchronized的代码块中,并且必须是相同的锁对象才行。

wait下的线程状态:WAITING

public class Test16 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker) {System.out.println("线程t1wait......");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程t1被唤醒!");}});Thread t2 = new Thread(()->{synchronized (locker) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2线程尝试唤醒t1......");locker.notify();}});t1.start();t2.start();}
}

运行结果:

在线程wait期间,该线程是主动放弃了CPU资源的,是解锁状态,暂时不会参与锁竞争。

这种情况下,当使用notify唤醒时只能唤醒其中一个,并且是随机的,这就有很大的不确定性在里面,所以java标准库中还提供了notifyAll方法,能够唤醒所有相同锁对象的wait。

对于notify是随即唤醒这一点还有可能会造成线程饿死,所谓线程饿死也就是某个线程长时间没有吃到CPU的资源。

public class Test17 {public static void main(String[] args) {Object l1 = new Object();Thread t1 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t1被唤醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t2被唤醒!");}    catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t3 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t3被唤醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t4 = new Thread(()->{System.out.println("输入任意内容唤醒所有线程:");Scanner sc = new Scanner(System.in);sc.next();synchronized (l1){l1.notifyAll();}});t1.start();t2.start();t3.start();t4.start();}
}

运行结果:

 

相关文章:

  • 长难句。。
  • Kafka消息队列之 【消费者分组】 详解
  • maven 安装 本地 jar
  • 紫禁城多语言海外投资理财返利源码带前端uniapp纯工程文件
  • 带你玩转 Flink TumblingWindow:从理论到代码的深度探索
  • DMC-1410/1411/1417USER MANUAL 手侧
  • 视频编解码学习8之视频历史
  • 艾体宝方案丨深度解析生成式 AI 安全风险,Lepide 为数据安全护航
  • 垃圾回收的三色标记算法
  • Petalinux开发Linux
  • 最新CDGP单选题(第四章)补充
  • fastjson2 json.tojsonstring 会自动忽略过滤掉 key: null的数据
  • Linux Shell编程之条件语句
  • SGLang 实战介绍 (张量并行 / Qwen3 30B MoE 架构部署)
  • 红黑树详解初版
  • 公链钱包开发:技术逻辑与产品设计实践
  • Asp.Net Core IIS发布后PUT、DELETE请求错误405
  • 飞云分仓操盘副图指标操作技术图文分解
  • Ceph PG unfound/lost 问题排查与解决
  • 101alpah_第5个alpha学习
  • 印巴冲突升级,巴防长称已击落5架印度战机
  • 48岁黄世芳履新中国驻毛里求斯大使,曾在广西工作多年
  • 贵州黔西市游船倾覆事故发生后,多家保险公司紧急响应
  • 国内外数十支搜救犬队伍齐聚三明,进行废墟搜救等实战
  • 四人自驾游宣恩因酒店爆满无处住宿,求助文旅局后住进局长家
  • 辛涛任山东第一医科大学副校长,曾为“博士服务团”成员