目录
同步锁的引入:
无同步:
输出结果:
加同步锁:
输出结果:
解决办法:
方法一:
方法二:
输出结果:
同步锁机制:
同步锁机制:
synchronized的锁是什么?
注意:
同步的范围:
1、代码是否存在线程安全
2、如何解决
切记:
锁的释放:
释放锁的操作:
不会释放锁的操作:
单例模式-懒汉式-双重加锁校验:
第一次判断singleton是否为null
第二次判断singleton是否为null
线程的死锁问题:
死锁:
产生死锁的四个必要条件
解决方法:
Lock锁:
synchronized 与 Lock 的对比:
同步锁的引入:
java中cpu分给每个线程的时间片是随机的并且在java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。比如下面程序:
无同步:
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
thread1.setName("线程一");
thread2.setName("线程二");
thread3.setName("线程三");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyThread extends Thread{
static int tick = 10;
public MyThread() {
}
@Override
public void run() {
while (true) {
if (tick > 0) {
System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
}
}
}
}
输出结果:
线程一 剩余tick:9
线程一 剩余tick:9
线程一 剩余tick:5
线程一 剩余tick:4
线程一 剩余tick:3
线程二 剩余tick:7
线程二 剩余tick:6
线程三 剩余tick:8
线程二 剩余tick:2
线程一 剩余tick:1
可以看到票数不是按照顺序减少,票刚开始卖时,有两个线程同时进入临界区,导致票买了两张,总数却只减少了一张。
加同步锁:
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
thread1.setName("线程一");
thread2.setName("线程二");
thread3.setName("线程三");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyThread extends Thread{
static int tick = 10;
public MyThread() {
}
@Override
public synchronized void run() {
while (true) {
if (tick > 0) {
System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
}
}
}
}
输出结果:
线程一 剩余tick:9
线程一 剩余tick:6
线程一 剩余tick:5
线程一 剩余tick:4
线程一 剩余tick:3
线程二 剩余tick:8
线程二 剩余tick:1
线程三 剩余tick:7
线程二 剩余tick:0
线程一 剩余tick:2
这时一种非常典型的同步锁错误,以为自己加了锁,其实锁加在了各自对象的非静态方法上,即对同一个对象,这把锁才有用,我们创建了三个不同的对象,三个有各自的方法,此时锁方法无法锁住任何临界区。
解决办法:
- 让MyThread实现Runnable接口,只创建一个对象,将同一个对象作为参数传给Thread的构造器。
- 在run方法中用MyThread.class锁住代码块。
方法一:
public class Main {
public static void main(String[] args) {
MyThread mt = new MyThread();//只创建一个MyThread对象
Thread thread1 = new Thread(mt);
Thread thread2 = new Thread(mt);
Thread thread3 = new Thread(mt);
thread1.setName("线程一");
thread2.setName("线程二");
thread3.setName("线程三");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyThread implements Runnable{//实现Runnable接口
static int tick = 10;
public MyThread() {
}
@Override
public synchronized void run() {
while (true) {
if (tick > 0) {
System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
}
}
}
}
方法二:
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
thread1.setName("线程一");
thread2.setName("线程二");
thread3.setName("线程三");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyThread extends Thread{
static int tick = 10;
public MyThread() {
}
@Override
public void run() {
while (true) {
synchronized (this.getClass()) {
if (tick > 0) {
System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
}
}
}
}
}
输出结果:
线程一 剩余tick:9
线程一 剩余tick:8
线程一 剩余tick:7
线程一 剩余tick:6
线程一 剩余tick:5
线程一 剩余tick:4
线程一 剩余tick:3
线程一 剩余tick:2
线程一 剩余tick:1
线程一 剩余tick:0
如此这般,票数才能按照顺序减少,而不会一个消费者看到窗口还有八张票,另一个消费者看到还有2张票。也不会出现两个消费者同时买到同一张票
同步锁机制:
同步锁机制:
同步机制中的锁在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
同步的范围:
1、代码是否存在线程安全
(1)明确哪些代码是多线程运行的代码,如窗口
(2)明确多个线程是否有共享数据,如车票
(3)明确多线程运行代码中是否有多条语句操作共享数据,run方法
2、如何解决
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
锁的释放:
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,陈果老师说过,带锁睡眠是多线程编程的大忌
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
单例模式-懒汉式-双重加锁校验:
class Singleton {
private static Singleton instance = null;
private Singleton(){//私有化构造方法}
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance == null){
instance=new Singleton();
}
}
}
return instance;
}
}
第一次判断singleton是否为null
第一次判断是在Synchronized同步代码块外进行判断,由于单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以,第一次判断,是为了在singleton对象已经创建的情况下,避免进入同步代码块,提升效率。
第二次判断singleton是否为null
第二次判断是为了避免以下情况的发生。
- 假设:线程A已经经过第一次判断,判断singleton=null,准备进入同步代码块.
- 此时线程B获得时间片,犹豫线程A并没有创建实例,所以,判断singleton仍然=null,所以线程B创建了实例singleton。
- 此时,线程A再次获得时间片,犹豫刚刚经过第一次判断singleton=null(不会重复判断),进入同步代码块,这个时候,我们如果不加入第二次判断的话,那么线程A又会创造一个实例singleton,就不满足我们的单例模式的要求,所以第二次判断是很有必要的。
线程的死锁问题:
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
new Thread() {//匿名内部类
@Override
public void run() {
synchronized (s1) {
s2.append("A");
synchronized (s2) {
s2.append("B");
System.out.print(s1);
System.out.print(s2);
}
}
}
}.start();
new Thread() {//匿名内部类
@Override
public void run() {
synchronized (s2) {
s2.append("C");
synchronized (s1) {
s1.append("D");
System.out.print(s2);
System.out.print(s1);
}
}
}
}.start();
死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
产生死锁的四个必要条件
1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
4.循环等待:发生死锁时,线程进入死循环,永久阻塞。
解决方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
Lock锁:
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
private final ReentrantLock lock = new ReentrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{//注意:如果同步代码有异常,要将unlock()写入finally语句块
lock.unlock();
}
}
}
synchronized 与 Lock 的对比:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)所以优先使用顺序:Lock -> 同步代码块->同步方法