阅读《Java高并发编程详解》后的笔记。
线程安全与数据同步
上篇中,模拟营业大厅叫号机,程序设计最大号码是50,运行结果很正常,但是如果变成500,多次运行可能出现以下问题:
- 某个号码略过没有出现
- 某个号码多次显示
- 号码超过了500
/**
* 模拟营业大厅叫号机
*
*/
public class TicketRunnable implements Runnable{
private int index = 1;
private final static int MAX = 500;
@Override
public void run() {
while(index <= MAX){
System.out.println(Thread.currentThread() + "的号码是" + (index++));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
final TicketRunnable ticketRunnable = new TicketRunnable();
Thread thread1 = new Thread(ticketRunnable,"一号窗口");
Thread thread2 = new Thread(ticketRunnable,"二号窗口");
Thread thread3 = new Thread(ticketRunnable,"三号窗口");
Thread thread4 = new Thread(ticketRunnable,"四号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
这里,产生了数据不同步的问题,其原因可结合JAVA内存模型以及CPU缓存知识来分析,这两部分内容单独进行记录。
一句话,产生以上这些情况,是因为多个线程对index(共享资源)同时操作引起的,解决这个问题需使用synchronized关键字。
synchronized
1、简介
synchronized可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来解决:
(1)提供一种锁机制,确保共享资源的互斥访问;
(2)包括monitor enter和 monitor exit两个JVM指令,保证任何时候任何线程执行到 monitor enter成功之前,必须从主内存获取数据,而不是缓存,monitor exit运行成功之后,共享变量被更新的值必须刷入主内存(JAVA内存模型以及CPU缓存知识);
(3)遵循java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter(JAVA内存模型以及CPU缓存)。
2、用法
用于对代码块或方法进行修饰,不能对class以及变量进行修饰。
上面的代码使用synchronized,问题可以解决:
public class TicketRunnable implements Runnable{
private int index = 1;
private final static int MAX = 500;
private final static Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX){
//获取与mutex相关联的monitor锁
while(index <= MAX){
System.out.println(Thread.currentThread() + "的号码是" + (index++));
}
}
}
3、原理
如果对上面的class文件进行反汇编,会出现monitor enter 和monitor exit指令。
(1)monitor enter
每个对象都与monitor关联,一个monitor的锁只能被一个线程在同一时间获得,一个线程尝试获得与对象关联的monitor所有权时会发生如下事情:
(2)monitor exit
当某个线程获得过与某个对象关联的monitor的所有权时,释放monitor的所有权就是将monitor的计数器减一。如果计数器结果为0,该线程就不再拥有对该monitor的所有权。
(3)This monitor 与class monitor
synchronized同步类的不同实例方法/静态方法,争抢的是同一个monitor的lock,与之关联的引用是This monitor/class monitor的实例引用。
public synchronized void method() -->this monitor
public static synchronized void methid() -->class monitor
4、使用synchronized 注意的问题
(1)与monitor关联的对象不能为空
private final Object MUTEX = null;
public void syncMethod(){
synchronized (MUTEX){
}
}
每一个对象和一个monitor关联,对象为空,monitor无从谈起。
(2)作用域要适当
synchronized 尽量作用于共享资源的读写作用域,如果作用域越大,代表其效率越低,甚至失去并发优势。
(3)不同的monitor锁相同的方法
public class TicketRunnable implements Runnable{
private final Object MUTEX = new Object();
@Override
public void run() {
//...
synchronized (MUTEX){
//...
}
//...
}
public static void main(String args[]){
for (int i=0;i<5;i++){
TicketRunnable ticketRunnable = new TicketRunnable();
Thread tread = new Thread(ticketRunnable);
tread.start();
}
}
}
上面的代码构造了5个Runnable的实例,Runnable作为线程执行单元传递给Thread,每一个线程争抢的monitor关联引用都是独立的,而线程之间进行monitor lock的争抢只能发生在与monitor关联的同一个引用上,所以上面synchronized不能起到互斥的作用。
(4)多个锁交叉导致死锁
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read(){
synchronized (MUTEX_READ){
synchronized (MUTEX_WRITE){
//...
}
}
}
public void write(){
synchronized (MUTEX_WRITE) {
synchronized (MUTEX_READ) {
//...
}
}
}
5、程序死锁
(1)交叉锁 哲学家吃面
(2)内存不足
(3)一问一答式的数据交换,服务端和客户端吧都在等待双方发送数据
(4)数据库锁 某线程执行 for update退出事务,其他线程访问死锁
(5)死循环引起 程序不工作,cpu占有率居高不下(死锁)
HashMap-->线程非安全
ConcurrentHashMap或者Collections.synchronizedMap-->线程安全