- synchronized特性
- synchronized使用
- 修饰普通方法(对象锁)
- 修饰静态方法(类锁)
- 修饰代码块(明确指定锁的对象)
- 非锁竞争情况
- 死锁
- 死锁是什么?
- 死锁的必要条件
- 循环等待场景
- 程序死锁怎么排除
- 死锁问题怎么解决
- 标准库的线程安全类
- Java多线程是如何实现数据共享
前面介绍到线程安全问题:线程安全问题;线程安全问题怎么解决呢?synchronized是一大重点
synchronized特性
synchronized是个对象加锁;进入 synchronized 修饰的代码块, 相当于加锁;退出 synchronized 修饰的代码块, 相当于解锁。这个锁是存在对象头里的;可以理解, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态;加锁了则表示有人。
1:互斥:
理解为每一把锁内部维护一个等待对象;锁被某个线程占用;其它线程尝试进行加锁就会阻塞等待。等改线程释放锁后;才由操作系统唤醒被阻塞的线程来获取这个锁。
注意:假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁,此时 B 和 C 都在阻塞队列中排队等待. 但是B和C谁先获取到这把锁是不知道的;看操作系统怎么调度。
2:刷新内存
synchronized 也能保证内存可见性的问题;当一个线程进入一个Synchronized方法或块时,它会获取一个锁,并且在释放锁之前,会将其所有的本地内存的改动刷新到主内存中。同时,当另一个线程进入一个Synchronized方法或块时,它会从主内存中读取变量的最新值到自己的本地内存。
3:可重入
同一个线程两次加同一把锁;不会死锁。比如:你在上厕所把门锁了;突然有人时空回溯把你传回5分钟前在厕所门口的情况;然后你发现门是锁的。synchronized就是自己带了钥匙。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息;当发现加锁时占用锁的是自己可以继续获取
到锁, 并让计数器自增。解锁的时候计数器递减为 0 的时候, 才真正释放锁.;才能被别的线程获取到。
synchronized使用
修饰普通方法(对象锁)
public class SynchronizedDemo {
public synchronized void methond() {
}
修饰静态方法(类锁)
public class SynchronizedDemo {
public synchronized static void method() {
}
}
锁定的是类的Class对象,而不是类的某个实例。
如果现在一个类有两个方法;add1和add2都是静态方法;其中add1这个方法加了synchronized。如果我两次线程一个调用add1一个调用add2;会不会锁竞争?
当然不会;因为一个线程在加锁;一个线程在不加锁这个类对象。
修饰代码块(明确指定锁的对象)
public class SynchronizedDemo {
public void method() {
Object obj=new Object();
synchronized (obj) {
}
}
}
总结:两个线程针对同一个对象加锁才会产生锁竞争;有锁竞争线程才安全;一定是锁的对象相同。
修饰普通方法:
加锁的是实例对象,谁调用谁就是加锁的对象,线程里也是通过对象调用这个方法的。
修饰静态方法:
Synchronzied 修饰静态方法==》其实是类锁,因为是静态方法,它把整个类锁起来了; 两个线程都用这个类名.方法名;那就会锁竞争。
非锁竞争情况
情况1:一个加锁一个没加锁:;虽然是同一个对象;但是线程2没加锁就起不到一个阻塞的效果
class counter3{
static int count=0;
synchronized public static void add1(){
count++;
}
public static void add2(){
count++;
}
} //执行结果是100000
public class test3 {//一个加锁,一个不加锁情况
public static void main(String[] args) throws InterruptedException {
counter3 c3=new counter3();//修饰静态方法时是类锁,即便用两个不同的对象调用,最终调用的还是类名.方法。
counter3 c33=new counter3();
Thread t1=new Thread(()->
{
for (int i = 0; i <50000 ; i++) {
c3.add1();//调用的时候就会进行加锁
}
}
);
Thread t2=new Thread(()-> {
for (int i = 0; i <50000 ; i++) {
c33.add2();
}
}
);
t1.start();
t2.start();
t1.join();//等待线程执行完再去打印count的值
t2.join();
System.out.println(counter3.count);
}
}
总结:
一个有锁;一个无锁;没有起到原子性线程安全的意义。相当于都没加锁。一个add1有修饰,一个add2没有修饰。两个线程用c对象分别调用这两个方法,都是执行count++。所以它们还是同步执行的;会原子性问题。
情况2:同一个对象;多线程下操作同一个变量一个有锁;一个无锁。
class counter{
int count=0;
synchronized public void add1(){
count++;
}
public void add2() {
count++;
}
}
public class test1 {//线程安全问题测试。
public static void main(String[] args) throws InterruptedException {
counter c=new counter();
Thread t1=new Thread(()->
{
for (int i = 0; i <50000 ; i++) {
c.add1();
}
}
);
Thread t2=new Thread(()->
{
for (int i = 0; i <50000 ; i++) {
c.add2();
}
}
);
t1.start();
t2.start();
t1.join();//等待线程执行完再去打印count的值
t2.join();
System.out.println(c.count);
}
}
就像这个代码,相当于都没加锁的效果,结果依然是不确定的。
三条结论:
1:两个线程针对同一个对象加锁,锁竞争能解决我们上述问题
2:两个线程针对不同对象加锁,不会锁竞争,解决不了上述问题
3:两个线程一个加锁一个不加锁。不会锁竞争,解决不了上述问题(同一个对象;两个方法add1、add2都操作static的count++;而一个有加锁、一个没加锁;那就出问题)
加锁本质上是cpu提供加锁这样的指令,操作系统实现锁,操作系统锁的api提供给JVM,JVM就提供给synchronized
死锁
为了解决线程安全问题引入锁的概念;然后引入锁的概念就可能会出现死锁的问题
死锁是什么?
死锁指的是两个或多个运行的线程或进程因争夺资源而造成的一种僵局。比如:所有线程或进程都在等待某个资源,但这个资源又被另一个线程或进程占有,因此无法继续执行。
死锁的必要条件
1:互斥使用;当资源(锁)被一个线程占有时,别的线程不能使用。(如果别人都能用了;那肯定就不会死锁)
2:不可抢占;当别人把这个资源(锁)占有;你只能等它主动释放;不能把它的给抢了。
3:持有和等待:当资源请求者在请求其他的资源的同时保持对原有资源的占有。比如:线程1请求这个被线程2占用的锁;当我线程2占用这个锁;我去请求线程3的资源;线程1得一直等着;线程2不会释放这个锁的。
4:循环等待;存在一个等待循环,每个线程都在等待下一个线程所持有的资源。比如:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。形成了一个等待环路。
循环等待场景
1:哲学家就餐问题:先不考虑卫不卫生的问题。5个哲学家5根筷子,哲学家两种状态:吃饭和思考。如果每个哲学家同时都拿着左手的筷子,并在等右边的筷子,就会有死锁的风险。都等不到对方释放左边筷子的时候。
2:两个线程两把锁,t1和t2加锁,互相获取对方的锁。都等不到对方释放锁的时候。
public class ThreadDemo14 {
public static void main(String[] args) {
Object x = new Object();
Object y = new Object();
Thread t1 = new Thread(() -> {
synchronized (x) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (y) {
System.out.println("abc");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (y) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (x) {
System.out.println("123");
}
}
});
t1.start();
t2.start();
}
}
3:一个线程,一把锁;连续加锁两次,如果锁是不可重入锁就会死锁。
为什么呢?
不可重入锁:锁不跟踪它是由哪个线程持有的;它只是简单地考虑是否被锁定。当你这个线程已经持有锁并且尝试再次加锁时;你只能慢慢的阻塞等等别人把这个锁释放;但是这个锁是你自己加的;你得自己释放;然后你把自己阻塞了;就不会有释放的那一天。比如自旋锁是不可重入的。
程序死锁怎么排除
比如:两个线程两把锁,t1和t2加锁,互相获取对方的锁
需要加个sleep才会出现这种情况。确保两个线程先把第一个锁获取到。因为这里如果不休眠一下,调度速度很快,线程1给第一个x加锁,然后马上就给y加锁。线程2就先进不去阻塞等待到线程1的解锁再继续加。如果不加sleep就产生不了相互访问锁的情况。
死锁是很隐蔽的一个问题;在程序不容易测试出来;所以我们可以在jconsole查看一下线程在搞什么飞机。
可以看到两个对方都显示在阻塞状态,并且告诉你阻塞在14行和26行。等待数,表示等待一个线程的解锁。
怎么解决这个问题呢?
死锁问题怎么解决
给锁加一个编号按顺序加锁;上面的问题是;线程1先给x加锁后给y加锁;线程2先给y加锁再给x加锁。如果两个线程同时先给x加锁;再给y加锁;这样子另一个线程就会等待先给x加锁的释放后才能加上锁。x锁释放了;y就自然也释放了。阻塞在第一步。
标准库的线程安全类
多个线程操作同一个集合类就需要考虑到线程安全问题。
一般都是我们需要加锁的自己加,因为加锁会有额外的时间开销。String虽然没加锁,但是它是不可变性,也是线程安全的。
Java多线程是如何实现数据共享
JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享一份的;只要把某个数据放到堆内存中, 就可以让多个线程都能访问到。