目录
一、synchronized关键字特性
1、互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。
进入synchronized修饰的代码块,相当于加锁;而退出synchronized修饰的代码块,相当于解锁。
synchronized会指定一个“锁对象”,当两个线程尝试对一个对象加锁,此时就会出现“锁冲突”“锁竞争”,一旦竞争出现,一个线程能够拿到锁才继续执行代码;拿不到锁就只能阻塞等待,等到前一个线程释放锁后才能拿到锁继续执行。本质上就是把“并发执行”转换成“串行执行”,这样就不会出现“穿插”的情况。
synchronized用的锁是存在Java对象头里的,那什么是对象头呢?
java的一个对象,对应的内存空间中,除了自己定义的一些属性之外,还有一些自带的属性, 这个自带的属性就是对象头。在对象头中,其中就有属性表示当前对象是否已经加锁。
对象头是所有对象实例所共有的一部分,一个实例加了锁,共有的对象头里面相关的标志就被设置了,其他对象实例加锁的时候,看到对象头里面相关标志,就知道其他对象实例再用锁,自己就加不了锁了。
可以粗略理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的"锁定"状态(类似于厕 所的"有人/无人")。如果当前是"无人"状态,那么就可以使用,使用时需要设为"有人"状态;如果当前是"有人"状态,那么其他人无法使用,只能排队。
针对每一把锁,操作系统内部都维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
注意:
上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来"唤醒",这也就是操作系统线程调度的一部分工作。
假设有ABC三个线程,线程A先获取到锁然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待。但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则。
2、刷新内存
synchronized的工作过程:
获得互斥锁
从主内存拷贝变量的最新副本到工作的内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
所以synchronized能保证内存可见性。
3、可重入
我们先来辨别一下什么是可重入锁和不可重入锁:
- 不可重入锁
不可重入锁。简单理解为“把自己锁死”,一个线程没有释放锁,然后又尝试再次加锁。即一个线程获取一个到一个锁之后,当这个线程再次获取这个锁,需要等待这个线程释放锁之后才能获取,这很明显就是阻塞了嘛。
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁。但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作。这时候就会死锁。
我们来实现一下呢:
class NotReentrantLock{
private static boolean isLock = false;
// 加锁
public synchronized void lock() throws InterruptedException {
//判断是否加锁
while (isLock){
wait();
}
//锁住当前线程
isLock = true;
}
// 解锁
public synchronized void unLock(){
isLock = false;
notify();
}
}
public class Demo1 {
private static NotReentrantLock notReentrantLock = new NotReentrantLock();
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
notReentrantLock.lock();
dosomething();
notReentrantLock.unLock();
}
public static void dosomething() throws InterruptedException {
notReentrantLock.lock();
System.out.println("dosomething...");
notReentrantLock.unLock();
}
}
我们只要运行上面的代码,就会被卡住,test方法先加锁,然后调用dosomething,dosomething再加锁,但是锁被test方法先拿到了,dosomething啥都干不了,等着test释放锁,test又等着dosomething做完事情,才释放锁,这样,就卡死了。
- 可重入锁
当某个线程试图获取一个自己已经持有的锁时,那么会立刻获得这个锁,不用等待,可以理解为这个锁可以被继承。
可重入锁的内部,包含了"线程持有者"和"计数器"两个信息,如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增;解锁的时候计数器递减为0的时候,才真正释放锁。(才能被别的线程获取到)
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,我们看下如下代码:
public class Demo1 {
static Object lock = new Object();
public static void main(String[] args){
test();
}
public static void test(){
synchronized (lock){
dosomething();
}
}
public static void dosomething() {
synchronized (lock){
System.out.println("dosomething...");
}
}
}
运行结果:
二、synchronized使用方法
synchronized本质上要修改指定对象的"对象头"。从使用角度来看,synchronized也势必要搭配一个具体的对象来使用。
1、直接修饰普通方法
synchronized修饰一个普通方法很简单,就是在方法的前面加synchronized。修饰方法范围是整个函数。
在实例的非静态方法上使用synchronized关键字时,它会将该方法变成同步方法,相当于对当前实例对象(this)加锁,this作为对象监视器。这意味着只有一个线程可以同时执行该实例方法,以确保对该实例的互斥访问。当前类会创建多个实例对象,synchronized独立的控制每个实例对象的同步。
我们举个例子,Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间只能有一个人打开一个小房间,然后用完了还回去,再由JVM 去分配下一个获得钥匙的人。
同一个对象在两个线程中分别访问该对象的两个同步方法会产生互斥,因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。 不同对象在两个线程中调用同一个同步方法不会产生互斥,因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new一个对象,那么就会出现两个空间,两把钥匙。
我们运行如下代码:
class Counter {
public int count;
synchronized public void increase() {
count++;
}
//相当于
//public void increase() {
// synchronized (this) {
// count++;
// }
//}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
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);
}
}
运行结果:
2、修饰静态方法
在静态方法上使用synchronized关键字时,它会将该方法变为同步静态方法,相当于对当前类的Class对象加锁,当前类的Class对象作为对象监视器。这意味着只有一个线程可以同时执行该静态方法,以确保对该类的互斥访问。 当前类会创建多个实例对象,所以实例对应同一个静态方法,所以synchronized控制实例对象的同步。锁定的是类的Class对象,因此它会阻止不同实例以及静态方法之间的并发执行,因为它们共享相同的Class对象。
用类直接在两个线程中调用两个不同的同步方法会产生互斥,因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。用一个类的静态对象在两个线程中调用静态方法或非静态方法会产生互斥,因为是一个对象调用。
我们查看一下代码:
class Counter {
public static int count;
synchronized public static void increase() {
count++;
}
//相当于
//public static void increase() {
// synchronized (Counter.class) {
// count++;
// }
//}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
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);
}
}
运行结果:
总结一下,普通方法是富哥,每个实例对象都有各自的大房子,修饰普通方法只用看实例对象;静态方法是穷逼,所有实例对象挤在一个房子里面,静态方法相当于中央大厕所(房间没厕所),不同实例对象想用这个厕所得等其他实例用完。
3、修饰代码块
synchronized修饰代码块需要传入一个对象,此时synchronized加锁对象即为传入的这个对象实例。我们运行以下代码:
class Counter {
public static Object lock= new Object();
public static int count;
public void increase() {
synchronized (lock){
count++;
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter1.count);
}
}
运行结果:
三、volatile关键字
我们先来看一段代码:
import java.util.Scanner;
public class Demo1 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (isQuit == 0) {
// 循环体里啥都没干.
// 此时意味着这个循环, 一秒钟就会执行很多很多次.
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("请输入 isQuit: ");
Scanner scanner = new Scanner(System.in);
// 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
isQuit = scanner.nextInt();
});
t2.start();
}
}
运行结果:
啊?卡住了,输入了一个1然后卡住了!!为什么啊?
cpu使用这个变量时,会把这个内存中的数据,先读出来,放到cpu的寄存器中,代码里面那个while循环,速度太快,每次都从内存中读取的话就太慢了。为了解决上述问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器,减少读内存的次数,也就可以提高整体程序的效率了。
此处的问题,就是“内存可见性”情况引起的。编译器优化错了。
其实while循环中加个sleep也能解决,因为编译器还没有优化。
import java.util.Scanner;
public class Demo1 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (isQuit == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("请输入 isQuit: ");
Scanner scanner = new Scanner(System.in);
// 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
isQuit = scanner.nextInt();
});
t2.start();
}
}
volatile能保证内存可见性。就是告诉编译器,不要优化不要优化不要优化!
代码在写入volatile修饰的变量的时候,将改变线程工作内存中volatile变量副本的值并且将改变后的副本的值从工作内存刷新到主内存。
代码在读取volatile修饰的变量的时候,将从主内存中读取volatile变量的最新值到线程的工作内存中,然后从工作内存中读取volatile变量的副本。
volatile和synchronized有着本质的区别,synchronized能够保证原子性,而volatile保证的是内存可见性,不保证原子性。
我们运行如下代码:
class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
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);
}
}
运行结果:
可以看到volatile不保证原子性。
synchronized既能保证原子性,也能保证内存可见性,我们对上面的代码进行调整:
import java.util.Scanner;
public class Demo1 {
static Object lock =new Object();
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (lock){
if(isQuit != 0){
break;
}
}
}
System.out.println("t1 退出!");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("请输入 isQuit: ");
Scanner scanner = new Scanner(System.in);
// 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
isQuit = scanner.nextInt();
});
t2.start();
}
}
运行结果:
标签:JAVA,Thread,synchronized,对象,基础,线程,new,多线程,public From: https://blog.csdn.net/2401_84064192/article/details/137409945