目录
1. synchronized的特性
2. synchronized的使用
3. Java标准库中的线程安全类
1. synchronized的特性
(1)互斥:
前文已经介绍,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象,synchronized就会阻塞等待,进入synchronized修饰的代码块相当于加锁,退出synchronized修饰的代码块相当于解锁;
(2)刷新内存:
前文介绍内存可见性时已经提到:synchronized的工作过程是:
① 获得互斥锁,② 从内存拷贝变量的最新内存到寄存器,③ 执行代码,
④ 将更改后的共享变量的值刷新到寄存器,⑤ 释放互斥锁;
故而synchronized也可以保证内存的可见性;
(3)可重入:
同一个线程针对同一个锁连续加两次,如果出现了死锁就是不可重入,不会死锁就是可重入;
(3.1)死锁:
class Counter{
public int count=0;
synchronized public void increase(){
synchronized (this){
count++;
}
}
}
外层先加了一次锁,内层又对同一对象再次加锁,此时由于外层锁需要执行完内部代码才能解锁,而内层锁需要等待已经先锁的外层锁解锁后才能执行,此时就会形成死锁;
(3.2)可重入锁:
为了解决这个问题,JVM内部将synchronized实现为可重入锁。
可重入锁会记录当前占用锁的线程以及加锁次数,线程a第一次加锁成功后,锁内部就会记录当前占用锁的线程为a,同时加锁次数为1,后续线程a再加锁时,进行的加锁操作就非真实的加锁操作而是一个伪加锁,是没有实质影响的,只是将加锁次数增加为2;
代码执行完毕解锁时,会将计数-1,当锁的计数减到0时,才会真的解锁;
可重入锁降低了程序员的编写负担,降低了使用成本,提高了开发效率,但同时由于需要维护锁所属的线程以及加减计数会降低运行效率,程序的开销也会更大;
(3.3)死锁的必要条件:
① 互斥使用:一个锁被一个线程占用后,其他线程就无法占用;
② 不可抢占:一个锁被一个线程占用后,其他线程不能抢占该锁;
③ 请求与保持:当一个线程占据了多把锁之后除非显式释放锁,否则这些锁始终被该线程持有;
(以上三条都是锁本身的特点)
④ 环路等待:等待关系成环;
在实际开发中需要避免死锁,关键还是从避免环路等待入手:
针对多把锁加锁时约定好固定的顺序,就可以避免等待关系成环;
但实际情况中很少出现一个线程套锁的问题;
2. synchronized的使用
使用synchronized的本质是修改了Object对象中“对象头”内的一个标记;
(1)直接修饰普通方法:此时锁对象是this:
class Counter{
public int count=0;
synchronized public void increase(){
count++;
}
}
当两个线程同时对同一个对象进行加锁的时候才存在竞争;
(2)修饰一个代码块:需要显式指定锁对象:
class Counter{
public int count=0;
public void increase(){
synchronized (this){
count++;
}
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
counter.increase()表示针对this对象进行加锁操作;
注:任何对象都可以进行加锁是java语言的特色;
(3)修饰一个静态方法:
静态方法其实就是类方法,普通方法就是实例方法;故而
synchronized修饰一个静态方法就相当于针对当前类的类对象加锁;
class Counter{
synchronized public static void func1(){
}
public static void fun2(){
synchronized (Counter.class){
}
}
}
如在上文代码中,静态方法func1是表示为synchronized修饰的静态方法,其效果等效于静态方法func2;
注:(1)类对象就是运行程序时.class文件被加载到JVM内存中时的形态;
(2)使用synchronized很容易造成线程阻塞,一旦线程阻塞,此时放弃CPU,再次回到CPU的时间就不可控了,一旦代码中使用了synchronized,则“高性能”几乎无法实现;
3. Java标准库中的线程安全类
Java标准库中已经实现的类中有些是线程安全的,有些是线程不安全的:
线程不安全类:
①ArrayList ②LinkedList ③HashMap ④TreeMap ⑤HashSet ⑥TreeSet ⑦StringBuilder
线程安全类:
①Vector(不推荐) ②HashTable(不推荐)③ConcurrentHashMap ④StringBuffer ⑤String
注:(1)线程安全类由于一些关键方法都被synchronized修饰,保证了多线程环境下修改同一个对象不会出现线程不安全问题;
(2)String是线程安全类不是因为synchronized修饰,而是因为String是不可变对象,不存在多线程中修改造成的线程不安全问题;
同时请注意不可变对象与常量以及final没有必然联系:
不可变对象是指在该类中没有提供public的修改属性的方法,final修饰类表示类不可继承;