- 共享资源:指多线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就成为共享资源
- 数据同步/资源同步:保证多个线程访问到的数据是一致的
数据同步示例
public class TicketWindowRunnable implements Runnable{
private static final int MAX = 500;
private int index = 1;
@Override
public void run() {
printIndex();
}
public void printIndex(){
while (index <= MAX){
System.out.println("柜台" + Thread.currentThread() + "出票,票号" + index++);
}
/*
* 除了 为共享资源添加static关键词,也可实现Runnable接口的方式,但是这两种情况下当Max的值从50增长到500、1000还是会出现超出最大值的情况
* 因为共享资源依旧存在线程安全问题
* */
public static void main(String[] args) {
TicketWindowRunnable test = new TicketWindowRunnable();
Thread window1 = new Thread(test,"一号机");
Thread window2 = new Thread(test,"二号机");
Thread window3 = new Thread(test,"三号机");
Thread window4 = new Thread(test,"四号机");
window1.start();
window2.start();
window3.start();
window4.start();
}
}
- 上述代码可能产生:
- 1.某个号码被忽略:
线程1和2执行到index = 65处,线程2将index增1后不打印,此时又由线程1完成答应此时输出为67,则66将会被忽略
- 2.某个号码重复出现
- 3.某个号码
注:上述图片借鉴《Java高并发编程详解》p59-p60
synchronized关键字
- 出现共享资源线程同步问题时,可通过synchronized锁来解决,提供了一种排他机制,即同一时间只能有一个线程执行某些操作。
- 官方含义:关键字实现了一个简单的策略来防止线程干扰和内存一致性错误,如果该线程对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行
- 具体表现:
a. 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止不一致问题的出现
b.(不懂)关键字包括monitor enter 和 monitor exit两个JVM指令,它能够保证在任何时候线程执行到monitor enter成功之前都必须要从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存
c.关键字的指令严格遵守java happens-before 规则,一个monitor exit指令之前必定要有一个monitor enter - 用法:不能用于对class以及变量进行修饰
a.同步方法
b.同步代码块//[default|publilc|private|protected] synchronized [static] type method(); public synchronized void sync(){ ... } public synchronized static void staticSync(){ ... ... }
private finnal String NAME = "小北醒醒丶"; public void sunc(){ synchronized (NAME){ ... } }
- synchronized关键字提供了一种互斥机制,即同一时刻只能由一个线程访问同步共享资源
注意:
- 1.与monitor关联的对象不能为空
//mutex为null,则每个对象和一个monitor关联,对象都为null,则monitor则无,所以mutex不能为null
private final Object mutex = null;
public void synacMethod(){
synchronized(mutex){
//
}
}
- 2.sunchronized作用域太大
a.由于sunchronized关键字存在排他性,也就是说多有的线程必须串行经过sunchronized保护的共享区域,如果sunchronized作用域越大,则意味着效率越低。甚至失去并发的优势。
b.sunchronized关键字应该尽可能只作用与共享资源(数据)的读写作用域 - 3.不同的monitor企图锁相同的方法
错误示例:public static class Task implements Runnable{ private final Obeject MUTEX = new Object(); @Override public void run(){ //.. synchronized(MUTEX){ //.. } //.. } } public static void main(String[] args){ for(int i = 0;i < 5;i++){ new Thread(Task::new).start(); } }
- 上述代码中构造的五个线程的Runnable接口是五个实例,此时synchronized不能互斥与之对应的作用域。每一个线程争抢的monitor关联引用都是比起独立的,起不了互斥作用
多个锁的交叉导致死锁
- 代码示例:
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read(){
sunchronized(MUTEX_READ ){
sunchronized(MUTEX_WRITE ){
//..
}
}
}
public void write(){
sunchronized(MUTEX_WRITE ){
sunchronized(MUTEX_WRITE ){
//..
}
}
}
- 注:sunchronized修饰同一个类的不同方法,争抢的是同一个monitor
引起死锁的原因
- 交叉锁:
线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取R1的锁时,及哲学家吃面问题。 - 内存不足:
线程A与B需要内存30,线程A已占用10,线程B已占用20,剩余内存只存在20时,线程A与B都在等待彼此释放内存资源 - 一问一答式的数据交换:
客户端发送请求等待服务器响应,服务器由于某种原因没有收到请求,服务器等待客户端请求,此时都在等待,所以导致死锁 - 数据库锁:
某个线程执行for update语句退出了事务,其他线程访问该数据库时都将陷入死锁 - 文件锁:
某个线程获得了文件锁意外退出,其他读取该文件的线程也将会进入死锁直到系统释放文件句柄资源: - 死循环引起的死锁:
由于编码原因,进入死循环,CPU占用率居高不下,一般称为系统假死