count
的大小永远小于 10000,但也有可能小于 5000t1++
一次的过程中,t2++
两次,这样得到的结果,正常应该++3 次,而实际只++1 次综上,原因为:
线程在操作系统中,随机调度,抢占式执行
多个线程,同时修改同一个变量
修改操作不是“原子“的(比如++就是三个指令)
内存可见性
指令重排序
count++
变成原子的,也没有干预线程的调度,只不是通过这种加锁的方式,使一个线程在执行 count++
的过程中,其他的线程的 count++
不能插队进来synchornized
关键字,来完成加锁操作
- 这是一个关键字,不是函数,后面的() 并非参数,而是需要指定一个“锁对象”,然后通过“锁对象”来进行后续的判定
- ()
里的对象可以指定任何对象
- {}
里面的代码,就是要打包到一起的,()
里面还可以放任意的其他代码,包括调用别的方法啥的,只要是合法的 Java 代码,都是可以放的 public class Demo8 {
private static int count = 0;
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized(locker){
count++;
};
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
locker
对象加锁,t1 先加锁,就加锁成功了,于是 t1 继续执行 ()
里面的代码(进厕所,执行上厕所的操作)locker
对象已经被别人先锁了,就只能等(说明现在厕所有人,于是只能排队等待)unlock
一定是在 save
之后,确保了 t2 执行 load
的时候,t1 已经 save
double
这种内置类型写到() 里面,但是其他类型,只要是 Object
或者其子类,都可以。例如:一个字符串 s = “hello”
,一个链表 list
...... 都行注意:
join
的串行执行效率是要高很多的Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized(locker){
count++;
};
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
}
}
});count++
是串行执行的,而 for
循环、比较、i++
都是并发执行的如果是三个线程针对同一个对象加锁,也是类似的情况
此处
synchronized
是JVM
提供的功能,synchronized
底层实现就是JVM
中,通过C++
来实现的。进一步的,也是依靠操作系统提供的API
实现的加锁,操作系统的API
则是来自于CPU
上支持的特殊指令来实现的系统原生的加锁 API 和很多编程语言的加锁操作的封装方式是两个函数:
lock()
,unlock()
像 Java 这种通过
synchronized
关键字,来同时完成加锁/解锁的,比较少见 - 原生的这种做法,最大的问题在于unlock
可能执行不到,后面排队想用这个锁的就用不了(占着坑不拉屎) - Java 中的synchronized
是进入代码块就加锁,出了代码块就解锁,无论是return
还是抛出异常,不管以哪种方式出了代码块都会自动解锁,有效避免了没有执行解锁操作的情况
类对象:
synchronized()
里面synchronized()
还可以修饰一个方法
class Counter {
public int count = 0;
synchronized public void add() {
count++;
}
//等价于
public void add() {
synchronized (this) {
count++;
}
}
}
针对这个写法,锁对象就是
this
,谁调用add
谁就是锁对象在这里调用
add
的都是counter
,所以他们是同一个锁对象
synchronized public static void func() {
//错
}
//对
public static void func() {
synchronized (Counter.class)
}
static
方法没有this
static
方法也叫类方法,和具体的实例无关,只是和类相关此时
static
方法和类对象相关,此时的写法就是给类对象加锁
synchronized
就一定线程安全,还是得看代码咋写synchronized
的几种使用方式:
synchronized(){}
,() 里面指定锁对象synchronized
修饰一个普通的方法,相当于针对 this
加锁synchronized
修饰一个静态方法,相当于针对对应的类对象加锁理解锁对象的作用,可以把任意的 Object/Object
子类的对象作为锁对象。锁对象是啥不重要,重要的是两个线程的锁对象是否是同一个。是同一个才会出现阻塞/锁竞争,不是同一个是不会出现的
使用锁的过程中,一种典型的、严重的 bug
class Counter {
public int count = 0;
public void add() {
//随后调用add方法,又尝试对counter加锁
//但counter已经被加锁了,如果再次尝试对counter加锁,就会出现阻塞等待
synchronized (this){
count++;
}
}
}
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
//首先执行到这里,对counter加锁成功
synchronized (counter){
counter.add();
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (counter){
counter.add();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
synchronized
想要拿到锁,就需要外面的 synchronized
释放锁undefinedsynchronized
要释放锁,就需要执行到 }
}
,就需要执行完这里的 add
add
阻塞着呢~可重入锁:
synchronized
是“可重入锁”,针对上述一个线程连续加锁两次的情况做了特殊处理。C++/Python 中的锁就没有这样的功能
比如你向一个女生表白了,她同意了,那你就对她加锁了,她的持有人就是你
再有人对她加锁的时候,她就会进行判定,看当前这个要加锁的人,是不是她的持有人
若是隔壁老王找到她说“美女,我好喜欢你”,那她就会告诉他“你是个好人”
若是你找到她说“我好喜欢你”,那她就会说“我也好喜欢你”
//真加锁
synchronized (this) {
//直接放行,不会真加锁
synchronized (this){
}
}
若有两个线程 1 和 2,两把锁 A 和 B
线程 1 先针对 A 加锁,线程 2 针对 B 加锁
线程 1 不释放锁 A 的情况下,再针对 2 加锁;同时,线程 2 在不释放 B 的情况下针对 A 加锁
undefined
比如你和你女朋友出去吃饺子,她拿的醋,你拿的酱油
你说:你把醋给我,我用完了给你
她说:凭什么,你把酱油给我,我用完了给你
结果是你俩互不相让,僵持住了
public class Demo1 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker1){
System.out.println("t1 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1 加锁 locker2 完成");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
System.out.println("t2 加锁 locker2 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t2 加锁 locker1 完成");
}
}
});
t1.start();
t2.start();
}
}
打印结果:
t1 加锁 locker1 完成
t2 加锁 locker2 完成
代码中的
sleep
是为了确保 t1 和 t2 都先分别拿到了locker1
和locker2
,然后再分别拿对方的锁。如果没有sleep
,执行顺序就不可控,可能出现某个线程一口气拿到两把锁,另一个线程还没开始执行的情况,无法构造出死锁两个线程的第二次交叉加锁都没执行到,说明这两个线程都在第二次 synchronized 的时候阻塞住了,如果不人为干预,就会永远堵在这
image.png|659
死锁的四个必要条件:(缺一不可)
public class Demo1 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker1){
System.out.println("t1 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1 加锁 locker2 完成");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t2 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t2 加锁 locker2 完成");
}
}
});
t1.start();
t2.start();
}
}
输出结果:
t1 加锁 locker1 完成
t1 加锁 locker2 完成
t2 加锁 locker1 完成
t2 加锁 locker2 完成
locker1
,再上 locker2
就能解决这里的死锁问题了原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。