第二章 线程安全与共享资源竞争
2.1 synchronized同步介绍
防止出现资源并发冲突的解决思路如下:当共享资源被任务使用时,要对资源提前加锁。所有任务都采用抢占模式,即某个任务会抢先对共享资源加上第一把锁。如果这是一个排他锁,其他任务在资源被解锁前就无法访问了。如果是共享锁,当浏览某些数据时,其他任务也可以同时浏览,但是不允许修改。synchronized关键字的底层,相当于一个排他锁。
2.2 synchronized同步方法
多个线程同时访问同一个对象的某个方法时,如果该方法中存在对共享资源的操作,则可能引发线程安全问题。典型的共享资源有对象的成员变量、磁盘文件、静态变量等。
2.2.1 同步方法调用流程
synchronized同步方法,并不是真的给方法加了锁,它的本质是使用了当前对象的监视器锁,其调用流程如下:
(有个类叫Lock 他有个方法叫timeout() )
(1)线程A调用clock对象的timeout()方法,进入方法体前,先试图获取当前clock对象的监视器锁。
(2)如果clock对象的监视器锁没有被占用,则线程A会获取clock对象的监视器锁,然后进入timeout()方法;否则自旋等待获得锁(一般为抢占式,无须排队)。
(3)其他线程调用timeout()方法时,执行顺序相同。
2.2.2 同步方法之间的互斥
因为执行synchronized方法前必须要获得对象的监视器,同一个对象的多个synchronized方法共享同一个对象监视器,因此我们可以简单总结为,同一个对象的synchronized方法之间是互斥的。即线程A调用time()方法,会阻碍线程B调用timeout()方法。
2.2.3 同步方法与非同步方法
同一对象的同步方法与非同步方法不会产生影响。
2.3 synchronized同步静态方法
2.3.1 单例高并发问题
单例(Singleton)是非常有名的设计模式,就是在程序运行期间,确保只能存在一个对象。其特点如下:
• 构造函数私有化(不允许类外new对象)。
• 单例对象使用static存储。
• 调用单例对象时使用静态方法。
有多个线程同时访问单例模式的新建对象的静态方法的时候,很可能多个线程同时进入到了新建对象那边,那么就会产生多个对象,我们可以在新建对象的时候加一个同步锁就会防止这个问题。
2.3.2 类锁与对象锁
synchronized同步成员方法,本质使用的是当前对象的监视器锁。而synchronized同步静态方法,则使用的是当前Class的监视器锁。 java.lang.Object是所有对象的根,在Object的每个实例中都内置了对象监视器锁
2.3.3 静态同步方法之间互斥
静态同步方法之间是互斥的。
2.3.4 静态同步方法与静态非同步方法
静态同步方法和静态非同步方法之间互不干扰
2.4 synchronized同步代码块
synchronized同步方法,就是线程在调用方法前获取对象监视器锁,方法执行完毕后就释放对象锁。 方法同步的关键是为了保护共享资源,如果synchronized方法中没有使用共享资源,就无须使用synchronized同步这个方法。 在同步方法中,使用共享资源的只是部分代码。为了提高并发性能,一般没必要在整个方法的运行期都持有监视器锁。 使用同步代码块模式,可以在方法中真正需要使用共享资源时再获取监视器锁,共享资源访问结束马上释放锁,这样就节省了对象监视器锁的占用时间,可以有效提高并发性。
2.4.1 锁当前对象
synchronized (this) {}就是获取当前对象的监视器锁。这样可以使对象监视器的使用时间更短,并发性能更高了。
2.4.2 锁其他对象
监视器锁内置与Object的底层,所有的对象都源于Object,因此所有的对象都有监视器锁。使用其他Object对象的监视器锁,会比使用自身对象的监视器锁更加灵活。
synchronized(user){}
2.4.3 锁Class
不仅每个对象都有监视器锁,每个数据类型的class也内置了监视器锁。
synchronized(User.class){}。
synchronized(Object.class){}是类锁不是对象锁,如果项目中有其他的地方也用到了此类,就会造成并发冲突,因此,类锁尽量不要轻易使用。
合理使用类锁的基本原则,尽量使用当前类的监视器锁。
2.5 项目案例:火车售票
2.5.1 共享任务模式
一个Runnable就相当于一个任务
/**
* 售票任务
*/
public class TicketTask implements Runnable{
private Integer ticket = 30;
@Override
public void run() {
//这里的while保证了三个线程启动之后只要ticket>0就会一直运行,知道ticket<0结束。
//三个线程在while循环的时候会一直抢占式的竞争当前对象锁
while (ticket > 0 ){
//因为ticket的多线程共享资源,所以要用同步代码块进行保护
synchronized (this){
if (ticket > 0 ) {
System.out.println("窗口" + Thread.currentThread().getId() + "售出:" + ticket);
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
/**
* 窗口类
*/
public class Curtain extends Thread{
private String cno;
public String getCno() {
return cno;
}
/**
* public Thread(Runnable target) {
* init(null, target, "Thread-" + nextThreadNum(), 0);
* }
*/
public Curtain(Runnable target, String cno) {
super(target);
this.cno = cno;
}
}
public class main {
public static void main(String[] args) {
TicketTask ticketTask = new TicketTask();
new Curtain(ticketTask,"c01").start();
new Curtain(ticketTask,"c02").start();
new Curtain(ticketTask,"c02").start();
}
}
2.5.2 多任务模式
/**
* 共享任务模式是锁当前对象,而多任务模式是锁其他对象,
* (一般情况下多任务模式比共享模式执行的快) 可以这样理解一个任务相当于一只手,一个线程相当于一个人
* 三个任务三只手,一起执行,去获取锁,肯定比三个人一只手去获取锁执行的快
*/
public class TicketTask implements Runnable{
private static Integer ticket = 30;
private static Object obj = new Object();
private String eno;
public String getEno() {
return eno;
}
public void saleTicket() {
if (ticket >0){
System.out.println("窗口:" +Thread.currentThread().getId()+"售出"+ticket);
ticket--;
}
}
@Override
public void run() {
while (ticket > 0){
synchronized (obj){
saleTicket();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}]()
public class main {
public static void main(String[] args) {
new Thread(new TicketTask(),"co1").start();
new Thread(new TicketTask(),"co2").start();
new Thread(new TicketTask(),"co3").start();
}
}
2.6 项目案例
2.7 项目案例
2.8 JDK常见类的线程安全性
2.8.1 集合ArrayList 和Vector
Vector底层是静态数组结构
同一个对象的同步方法共享同一把锁,Vector的很多方法都用到了同步方法,所以在并发环境中用vector集合会严重影响并发性能,JDK为了优化vector的性能新增了ArrayList类
Arraylist没有使用同步访问机制,所以当ArrayList中添加数据的时候,另一个线程访问迭代访问集合中的元素的时候会抛出java.util.ConcurrentModificationException异常。
public class mainss {
public static void main(String[] args) {
List<Integer> strings = new ArrayList<>();
Integer i= 1;
new Thread(new Runnable() {
@Override
public void run() {
while (true){
strings.add(i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
strings.forEach(
System.out::println
);
}
}
}).start();
}
}
2.8.2 StringBuffer与StringBuilder
当需要拼接字符串的时候为了避免字符串的常量碎片,通常使用StringBuffer与StringBuilder。StringBuffer有同步机制,StringBuilder没有同步机制。
2.8.3 HashMap与concurrentHashMap
hashMap是一种数组加链表的数据结构,hashMap在put的时候会新建一个初始化长度固定的数组,数组里面储存的时候Node<K,V>,当hash值相同的时候,则通过链表的形式来继续进行储存,当链表的长度大于8的时候,就会把链表的形式装换成红黑树。
HashMap没有同步安全策略,concurrentHashMap则使用了大量的同步代码块,用于增加线程的安全性,并且为了线程安全并且并发性能高,他锁定的范围是很小的节点对象,而不是整个数组或者链表。