所有java的 锁机制都可以分为乐观锁和悲观锁,悲观锁是实实在在的锁,乐观锁更像是一种保证业务数据安全的机制。
乐观锁和悲观锁
乐观锁
在操作资源的时候,乐观地认为没有线程与自己争抢资源,所以不会加锁,而是通过无锁编程的逻辑去实现。如:在对更新资源前去判断,该资源有无被更新,没有则进行更新,有则另外操作。
常用:版本号机制;CAS自旋
悲观锁
在对资源进行操作时,悲观地认为一定有线程与自己争抢资源。在修改资源的时候,会一定确保只有自己能操作该资源。synchronized关键字和Lock的实现类都是悲观锁
悲观锁之synchronized
特性:
-
原子性:使用synchronized实现了同步,同步实现了原子性,保证了被同步的代码块在同一时间只有一个线程在执行。
-
可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
-
有序性:禁止代码重排序
主要作用体现:
实例对象:当前实例加锁,进入同步代码前获取当前实例的锁。
代码块:对括号中的对象进行加锁
静态方法:对类class进行加锁。简称类锁。
1,一个对象中如果有多个synchronized方法,同一时刻,只有有一个线程去调用其中一个synchronized方法了,其它线程都只能等待,不能进去当前对象的其它的synchronized方法。这里的锁是锁的当前对象。(对象锁)
样例(大家可以看看,猜一下会先打印哪一行,运行后再回过头读读上面一段话)
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
public class LockDemo {
public synchronized void dog() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("this is a dog!");
}
public synchronized void cat() {
System.out.println("this is a cat!");
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
new Thread(() -> {
lockDemo.dog();
}, "a").start();
new Thread(() -> {
lockDemo.cat();
}, "b").start();
}
}
2,锁只与有锁的方法有关,如果执行的是普通方法,那么与上情况无关(对象锁)
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
public class LockDemo {
public synchronized void dog() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("this is a dog!");
}
public synchronized void cat() {
System.out.println("this is a cat!");
}
private void fish() {
System.out.println("this is a fish!");
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
new Thread(() -> {
lockDemo.dog();
}, "a").start();
new Thread(() -> {
lockDemo.fish();
}, "b").start();
}
}
结果输出
3,锁与对象息息相关,两个对象,就不是同一把锁了(对象锁)
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
public class LockDemo {
public synchronized void dog() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("this is a dog!");
}
public synchronized void cat() {
System.out.println("this is a cat!");
}
private void fish() {
System.out.println("this is a fish!");
}
public static void main(String[] args) {
LockDemo lockDemo1 = new LockDemo();
LockDemo lockDemo2 = new LockDemo();
new Thread(() -> {
lockDemo1.dog();
}, "a").start();
new Thread(() -> {
lockDemo2.cat();
}, "b").start();
}
}
输出结果
4,如果将锁的方法改为静态同步方法之后,情况又有不同。
对于普通同步方法而言,锁的是当前的实例对象,通常指的是this,如代码 LockDemo lockDemo1 = new LockDemo();中的lockDemo1。
对于静态同步方法而言,锁的是类的class对象。如代码 LockDemo lockDemo1 = new LockDemo();中的LockDemo。
对于同步方法块,锁的是synchronized括号内的对象
例子
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
public class LockDemo {
public static synchronized void dog() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("this is a dog!");
}
public static synchronized void cat() {
System.out.println("this is a cat!");
}
private void fish() {
System.out.println("this is a fish!");
}
public static void main(String[] args) {
LockDemo lockDemo1 = new LockDemo();
LockDemo lockDemo2 = new LockDemo();
new Thread(() -> {
lockDemo1.dog();
}, "a").start();
new Thread(() -> {
lockDemo2.cat();
}, "b").start();
}
}
运行结果:
5,对象锁和类锁是两把不同的锁,对象锁锁的是实例对象本身,类锁锁的是类的class对象。
静态同步方法与普通同步方法之间是不存在竞争关系的。
例子
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
public class LockDemo {
public static synchronized void dog() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("this is a dog!");
}
public synchronized void cat() {
System.out.println("this is a cat!");
}
private void fish() {
System.out.println("this is a fish!");
}
public static void main(String[] args) {
LockDemo lockDemo1 = new LockDemo();
LockDemo lockDemo2 = new LockDemo();
new Thread(() -> {
lockDemo1.dog();
}, "a").start();
new Thread(() -> {
lockDemo2.cat();
}, "b").start();
}
}
运行结果 :
注意:一般请使用最小力度锁,能锁代码块,就不要锁对象,能锁对象就不要用类锁。
我们再来个例子加深一下对synchronized锁运用的理解。
package com.rojer.test;
import java.io.IOException;
public class MainTest {
private static /*volatile*/ boolean running = true;
private static void runs() {
System.out.println("start runs");
while (running) {
//System.out.println("Rojer");
}
System.out.println("runs end");
}
public static void main(String[] args) throws InterruptedException, IOException {
new Thread(MainTest::runs, "t1").start();
Thread.sleep(1000);
running = false;
System.in.read();
}
}
大家可以先拿出来,直接运行看看。
这块代码当它运行起来的时候,会发现它只会打印start runs,且运行不会停止
而当我们将while循环中的语句放开时就会出现runs end。
原因在于println方法,被synchronized修饰了。synchronized是保证可见性的,它会将线程缓存中的数据回刷(内存和缓存的更新),使线程获取到更新的值。
悲观锁之公平锁与非公平锁
公平锁:指定是多个线程按照申请锁的顺序进行排队获取锁。实现:ReentrantLock lock = new ReentrantLock(true);
非公平锁:指的是多线程获取锁的顺序并不是按照申请的顺序进行的,有可能后申请的反而会先执行,在高并发环境下可能会出现饥饿状态(饥饿指的是某个线程一直处理等待状态)
实现:ReentrantLock lock = new ReentrantLock(false); 或ReentrantLock lock = new ReentrantLock();
synchronized也是非公平锁。
思考:
1,为什么锁默认是非公平锁?
2,公平锁应用场景 有哪些?
样例(可自己手动改为公平锁试试):
package com.rojer.test.threadtest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private int number = 20;
ReentrantLock lock = new ReentrantLock(false);
public void sale() {
lock.lock(); // 操作资源前先加锁
try {
if (number > 0 ) {
System.out.println(Thread.currentThread().getName() + "卖出第:" + number-- + " 还剩余票: " + number);
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
for (int t = 0; t < 3; t++) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
lockDemo.sale();
}
}, "线程" + t).start();
}
}
}
非公平执行结果展示(结果不一定一致):
公平锁执行结果(结果不一定一致):
这个时候我们再回过头想想,为什么锁默认是非公平的?
我们知道程序对锁的切换是要耗费一定资源的,对我们人来说这个切换时间的消耗几乎感觉不到,但是对于cpu来说这个时间是非常明显的,所以非公平锁能更好的充分利用cpu的时间片,减少线程切换的开销,减少cpu的空闲时间。
悲观锁之可重入锁
概念:指的是同一个线程在外层方法获取锁的时候,在进入内存方法会自动获取锁(前提:锁的是同一个对象),不会因为之前已经获取过还没释放而阻塞。
ReentrantLock和synchronized都是可重入锁
样例:
package com.rojer.test.threadtest;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
// ReentrantLock lock = new ReentrantLock(true);
public synchronized void a() {
System.out.println(Thread.currentThread().getName() + "运行了a");
b();
}
public synchronized void b () {
System.out.println(Thread.currentThread().getName() + "运行了b");
c();
}
public synchronized void c() {
System.out.println(Thread.currentThread().getName() + "运行了c");
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
new Thread(() -> {
lockDemo.a();
}, "线程Rojer").start();
}
}
运行结果: