目录
一、线程安全
1.1 线程安全问题?
-
例如: 火车站售票例子,开启多线程同时售票,虽然设置了票数为static,但是还是会出现某一张票卖重复的情况
- —> 这就是线程不安全 —> 当前线程的数据会被别的线程篡改
-
为什么出现这种情况?
- 某个线程在执行自己的任务时,还没执行完,就被别的线程执行了;
- 如果多个线程执行的同一任务,操作同一数据,就会导致数据不一致
- 原因就是,线程任务没执行完,就被别的线程抢走
1.2 如何解决线程安全问题
线程不安全原因是线程任务没执行完,就被别的线程抢走
所以,解决方案就是 当前线程执行时,不要被抢走
方法
- 加锁!!! synchronized
具体如何实现?
- 使用synchronized修饰方法 --> 同步方法
- 使用synchronized修饰代码块 --> 同步代码块
1.3 同步方法
需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整
// 打印机类
public class Printer {
/**
* 方法1 打印1234
* 保证线程安全方式1: 给方法加锁synchronized
* 需要注意:
* 1) 需要同步的方法都要加锁
* 2) 锁的对象得是同一个
*/
public synchronized void print1() {
System.out.print(1 + " ");
System.out.print(2 + " ");
System.out.print(3 + " ");
System.out.print(4 + " ");
System.out.println( );
}
/**
* 方法1 打印ABCD
*/
public synchronized void print2() {
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.println( );
}
}
// 测试
public class TestPrinter {
public static void main(String[] args) {
Printer p = new Printer( );
// 开启一个线程
new Thread(){
@Override
public void run() {
while(true){
p.print1();
}
}
}.start();
// 又开启一个线程
new Thread(){
@Override
public void run() {
while(true){
p.print2();
}
}
}.start();
}
}
注意1: 需要同步的方法都要加锁,
测试print1()加synchronized, print2()不加,运行看效果,发现锁不住!
注意2: 锁的对象得是同一个
测试,创建两个Printer类对象,分别调用print1() 和print2()
会发现不同步…
原因就是,两个对象是两个this, 同步方法锁得是同一个对象!!! 所以没有锁住!!
可以给方法加static,静态的同步方法锁的是当前类的class文件,即虽然是两个Printer对象, 但是它俩都是Printer类的,即同一个class,照样可以锁住
1.4 同步代码块
需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整
// 打印机类
public class Printer2 {
private static Object lock = new Object();
/**
* 同步代码块,
* 1) 需要主动设置锁对象
* 2) 锁对象可以是任意对象
* 3) 同步的方法锁的得是同一个对象
*/
public void print1() {
synchronized (lock) {
System.out.print(1 + " ");
System.out.print(2 + " ");
System.out.print(3 + " ");
System.out.print(4 + " ");
System.out.println( );
}
}
public void print2() {
synchronized (lock){
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.println( );
}
}
}
// 测试
public class TestPrinter2 {
public static void main(String[] args) {
Printer2 p = new Printer2( );
Printer2 p2 = new Printer2( );
// 开启一个线程
new Thread(){
@Override
public void run() {
while(true){
p.print1();
}
}
}.start();
// 又开启一个线程
new Thread(){
@Override
public void run() {
while(true){
p2.print2();
}
}
}.start();
}
}
1.5 总结
- 区别:
- 同步方法: 对整个方法加锁,锁的范围较大
- 同步方法默认锁的是this
- 静态同步方法锁的是当前对象的字节码文件(class)
- 同步代码块: 对方法内部,部分代码加锁,范围较小
- 同步方法: 对整个方法加锁,锁的范围较大
- 相同:
- 需要同步的方法/代码都需要加锁
- 锁的都得是同一个对象,即得是同一把锁
1.6 售票例子
// 售票类
public class TicketWindow extends Thread {
// private static Object lock = new Object();
private static int ticketNum = 100;
public TicketWindow(String name) {
super(name);
}
/**
* 售票功能,需要并行执行(多线程)
*/
@Override
public void run() {
while (true) {
synchronized (TicketWindow.class) {
if (ticketNum > 0) {
System.out.println(this.getName( ) + "正在售出第" + ticketNum + "票");
// 模拟出票时间,稍微等待n毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace( );
}
ticketNum--;
} else {
System.out.println("票已售罄~");
break;
}
}
}
}
}
//测试
public class TestTicker {
public static void main(String[] args) {
new TicketWindow("窗口1").start();
new TicketWindow("窗口2").start();
new TicketWindow("窗口3").start();
new TicketWindow("窗口4").start();
}
}
1.8 补充
保证线程安全是使用synchronized,但是其实还有很多技术可以保证线程安全
- volatile 关键词
- ThreadLocal
- Lock
- ReentrantLock
- ReentrantReadWriteLock
- 悲观锁,乐观锁,公平锁,自旋锁…等等
二、线程安全的集合
学习常用类时,经常见到保证同步或者不同步的类
同步即安全,不同步即不安全
StringBuilder(不同步)和StringBuffer(同步)
ArrayList(不安全) 和 Vector(安全)
HashMap(不安全)和Hashtable(安全)
ConcurrentHashMap(线程安全),比Hashtable效率高
- 使用了同步代码块,锁了部分代码
- 采用的是分段锁机制,如果多个线程操作的是Hash表中不同的数据,不锁
- 如果操作的是同一个hash值下的数据,再锁
三、死锁【了解】
死锁是指在
多任务系统
中,各个任务由于竞争
资源而造成的一种互相等待
的现象,导致任务无法继续执行
的情况。死锁通常发生在多个任务同时需要多个共享资源,但是这些资源又只能被一个任务独占,但是另外一个任务不释放时!死锁通常发生在四个必要条件同时满足时:
- 互斥条件:资源只能被一个任务占用,其他任务需要等待释放。
- 占有且等待:任务至少持有一个资源,并且在等待获取其他资源。
- 不可抢占:已经分配给一个任务的资源不能被其他任务抢占,只能由持有资源的任务主动释放。
- 循环等待:一系列任务互相持有其他任务所需要的资源,形成一个循环等待的关系。
// 两把锁
public class MyLock {
public static final Object LEFT_LOCK = new Object();
public static final Object RIGHT_LOCK = new Object();
}
// 演示死锁
public class TestDeadLock {
public static void main(String[] args) {
new Thread("男朋友") {
@Override
public void run() {
synchronized (MyLock.LEFT_LOCK) {
System.out.println(this.getName( ) + "拿到左筷子");
synchronized (MyLock.RIGHT_LOCK) {
System.out.println(this.getName( ) + "拿到右筷子");
System.out.println(this.getName( ) + "吃饭");
}
}
}
}.start( );
new Thread("女朋友") {
@Override
public void run() {
synchronized (MyLock.RIGHT_LOCK) {
System.out.println(this.getName( ) + "拿到右筷子");
synchronized (MyLock.LEFT_LOCK) {
System.out.println(this.getName( ) + "拿到左筷子");
System.out.println(this.getName( ) + "吃饭");
}
}
}
}.start( );
}
}
为避免和解决死锁,可以采取以下策略:
- 避免死锁:通过破坏死锁的四个必要条件之一,来避免死锁的发生。比如,一次性获取所有需要的资源,或者按照一定的顺序获取资源。
- 检测和恢复:定期检测系统中是否存在死锁,一旦检测到死锁,采取恢复策略,比如中断一些任务,释放资源。
- 避免循环等待:为资源分配一个全局唯一的编号,任务按编号递增的顺序申请资源,释放资源则按相反的顺序进行,避免循环等待。
- 资源剥夺:当一个任务请求资源时,如果无法获取,可以暂时剥夺该任务已经持有的资源,让其他任务能够继续执行。
- 谨慎设计:在程序设计时,尽量避免使用多个资源互斥且不可抢占的情况,或者采取一些策略确保资源的合理分配和释放,减少死锁的发生可能性。
四、线程通信
线程通信是指线程间可以交互,指定信号,让线程执行或者等待
通过Object类中的方法完成通信
- wait()
- notify()
4.1 同步方法
需求: 两个输出的方法,保证正常输出不被打断且达到一人一次输出的效果
public class Printer {
// 定义一个信号量
// 1代表print1执行 2代表print2执行
private int flag = 1;
/**
* 线程通信的要求
* 1) 要保证线程安全
* 2) 线程等待方法是wait
* 线程唤醒方法是notify
* 3) 必须使用锁对象调用 通信的方法
* ---------
* 为什么,wait和notify这些线程通信的方法要设计在Object类?
* 答:
*/
public synchronized void print1() throws InterruptedException {
if (flag != 1){ // 信号不是1,说明不该print1执行,那就等待
this.wait();
}
System.out.print(1 + " ");
System.out.print(2 + " ");
System.out.print(3 + " ");
System.out.print(4 + " ");
System.out.println( );
// 改变信号量
flag = 2;
// 通知处于等待状态的线程启动
this.notify();
}
public synchronized void print2() throws InterruptedException {
if (flag != 2) { // 信号不是2,说明不该print2执行,那就等待
this.wait();
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.println( );
flag = 1;
// 通知处于等待状态的线程启动
this.notify();
}
}
public class TestPrinter {
public static void main(String[] args) {
Printer p = new Printer( );
// 开启一个线程
new Thread(){
@Override
public void run() {
while(true){
try {
p.print1();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}.start();
// 又开启一个线程
new Thread(){
@Override
public void run() {
while(true){
try {
p.print2();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}.start();
}
}
4.2 同步代码块
再使用同步代码块演示一遍,再次确定一个结论
- 锁对象是谁,就使用哪个对象来调用wait和notify
public class Printer2 {
// 定义一个信号量
// 1代表print1执行 2代表print2执行
private int flag = 1;
/**
* 线程通信的要求
* 1) 要保证线程安全
* 2) 线程等待方法是wait
* 线程唤醒方法是notify
* 3) 必须使用锁对象调用 通信的方法
* ---------
* 为什么,wait和notify这些线程通信的方法要设计在Object类?
* 答:
*/
public void print1() throws InterruptedException {
synchronized (Object.class) {
if (flag != 1) { // 信号不是1,说明不该print1执行,那就等待
Object.class.wait( );
}
System.out.print(1 + " ");
System.out.print(2 + " ");
System.out.print(3 + " ");
System.out.print(4 + " ");
System.out.println( );
// 改变信号量
flag = 2;
// 通知处于等待状态的线程启动
Object.class.notify( );
}
}
public void print2() throws InterruptedException {
synchronized (Object.class) {
if (flag != 2) { // 信号不是2,说明不该print2执行,那就等待
Object.class.wait( );
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.println( );
flag = 1;
// 通知处于等待状态的线程启动
Object.class.notify( );
}
}
}
补充: 目前这个代码可以保证两个线程通信,如果>= 3个线程,就不一定能按照预想顺序完成
原因是,线程过多,但是notify方法只能随机唤醒一个处于等待状态的线程
解决方案: 使用notifyAll
4.3 wait和sleep
- wait
- 是Object类中的方法
- wait会让线程等待
- wait方法必须在同步方法中使用
- wait方法方法线程等待时,会让出资源,别的线程可以执行
- sleep
- 是Thread类中的方法
- sleep会让线程等待
- 方法同步或者不同步都可以使用
- 如果线程不安全,使用了sleep,会让出资源,别的线程执行
- 如果线程安全,使用了sleep,不会释放资源,别的线程不会执行,会阻塞 --> 抱着锁睡
本篇的思维导图
最后
如果感觉有收获的话,点个赞 👍🏻 吧。
❤️❤️❤️本人菜鸟修行期,如有错误,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍