二、锁

线程同步

  • synchronized

  • Lock

    • ReentranLock
  • volatile

  • 局部变量 ThreaLocal

  • 阻塞队列

  • 原子变量

锁 (对象监视器)

synchronized锁

  1. 是什么锁?
  • 是一种互斥锁:一次只允许一个线程进入被锁住的代码块
  • 是一种内置锁/监视器锁
    • monitor lock(监视器锁) 工作原理
    • 只有在获取(acquire)锁成功之后 ,才能成为锁的拥有者(owner),通过调用wait()可以进入 等待区(wait set),方法执行完毕,线程退出临界区,释放监视锁
  1. 用处是什么?
  • 保证了线程的原子性
    • 只允许一个线程访问
  • 保证了线程的可见性
    • 执行完sync之后,修改的变量对其他线程是可见的
  1. 怎么使用?
  • 修饰普通方法
public class syncTest implements  Runnable {
    int count=0;
    //加锁
    @Override
    public synchronized void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+":"+count++);
        }
    }

    public static void main(String[] args) {
    //创建一个对象
        syncTest syncTest = new syncTest();
        //两个线程 一个对象
        Thread thread = new Thread(syncTest,"thread1");
        Thread thread2 = new Thread(syncTest,"thread2");
        thread.start();
        thread2.start();
    }
}
打印结果:
thread1:0
thread1:1
thread1:2
thread2:3
thread2:4
thread2:5
 public static void main(String[] args) {
 //同样的  如果是两个线程
        syncTest syncTest = new syncTest();
        syncTest syncTest2 = new syncTest();
        // 两个线程 两个对象
        Thread thread = new Thread(syncTest,"thread1");
        Thread thread2 = new Thread(syncTest2,"thread2");
        thread.start();
        thread2.start();
    }
result:
thread1:0
thread2:0
thread1:1
thread2:1
thread1:2
thread2:2
//不涉及到数据共享的情况  分别访问两个不同的对象

对于这个问题 我们可以用修饰动态静态方法 来解决

  • 修饰静态方法
    • 静态方法不属于当前实例,而是属于类。
public class syncTest implements Runnable {
//静态变量
    static int count = 0;

    @Override
    public  void run() {
        count();
    }
//修饰静态方法
    private synchronized static void count() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + count++);
        }
    }

    public static void main(String[] args) {
        syncTest syncTest = new syncTest();
        syncTest syncTest2 = new syncTest();
        Thread thread = new Thread(syncTest, "thread1");
        Thread thread2 = new Thread(syncTest2, "thread2");
        thread.start();
        thread2.start();
    }
}
打印结果:
thread2:0
thread2:1
thread2:2
thread1:3
thread1:4
thread1:5

// 虽然是两个对象实例,但是是count是正确的,也就是线程安全的。  但是thread1 2 不一定哪个先运行。
  • 修饰代码块
    • 使用场景:方法体比较大,需要同步的代码只是一小部分,如果整个方法同步,效率太低。
其他代码同上 

private void count() {
…… 省略
        synchronized (this) {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
            }
        }
    }
输入结果:
thread1:0
thread2:0
thread2:2
thread1:1
thread2:3
thread1:4
//是线程不安全的  因为this 指的就是调用这个对象的实例,上面两个线程调用两个不同的实例

解决上面问题的办法就是

//把this改成当前类 
synchronized (syncTest.class) {
    
}
//或者任意一个类都可以  不建议使用
synchronized (Object.class) {
    
}
  • this的效果和修饰方法体类似 是当前实例
  • xxx.class 效果和修饰静态类类似 是类
    4.释放锁
    -当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
    -当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
  1. 原理
    通过javap生成的class可以看见
image
    • Synchronized在JVM里可以通过成对的MonitorEnter和MonitorExit指令来实现
    • 执行到MonitorEnter尝试获取monitor的所有权,即获取该对象锁

sync锁存放结构图:

image

monitor运行机制图:

image
  • Monitor Record
    • 是线程私有的数据结构,每个线程都有一个Monitor Record列表。
    • Monitor Record结构包括
      • Owner:存放拥有该锁的线程的唯一标识
      • 其他……
  • java对象头
    • Synchronized使用的锁是放在java对象头里的,具体位置是对象头的MarkWord
    • MarkWord中的LockWord指向monitor record的起始地址
      具体参考

Lock

image
  1. Lock是一个接口,常用方法有
    • lock()
// 1.尝试获取锁,如果锁被其他线程获取,则处于等待状态
// 2.必须主动释放锁,发生异常时,也不会自动释放锁
// 3. 必须在 try catch中进行,在finally中释放锁,防止死锁发生
void lock(); 
可中断锁
//尝试获取锁,可以相应中断,可以中断线程的等待状态
void lockInterruptibly() throws InterruptedException;   
 
  • tryLock()
轮询锁
//1.尝试获取锁,获取锁成功则返回true,否则返回false  
//2.所以拿不到锁也会立即返回 不会等待
boolean tryLock();   

定时锁
//1. 可以设置等待时间
//2. 未获取锁之前被中断,则抛出异常   
boolean tryLock(long time, TimeUnit unit)  throws InterruptedException;   
//释放锁  一定要在finally块中释放
void unlock();   
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能
//一个锁可以有多个条件变量  
Condition newCondition();

Condition中await()/signal()/signalAll()分别对应synchronize中的wait()/notify()/notifyAll()

  1. ReentrentLock
  • 可重入锁 一个线程可以多次lock()
  • 独占锁(排它锁) 只允许一个线程获取锁
  • 公平/非公平锁 :只针对上锁过程
    • 公平锁:直接加入同步队列
    • 非公平锁(默认使用):尝试获取锁,成功则返回,失败则加入同步队列
 Lock lock = new ReentrentLock();
 
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

实现原理参考

synchronized与ReentrentLock对比
  • 性能
    • sync JDK1.5中效率较低,1.6以后有很多优化措施,加入了自适应自旋,锁消除,锁粗化,轻量级锁等
    • 1.6以后 建议使用sync
  • 用途
    • 都是可重入锁
    • ReentrantLock增加了一些高级功能:
      • 等待可中断:正在等待的线程可以放弃等待
      • 可实现公平锁
      • 锁可以绑定多个条件:可以同时绑定多个Condition()对象
  • 实现策略
    -sync:阻塞(互斥)同步,是一种悲观的并发策略。
    • 原理是,其他线程只能依靠阻塞来等待线程释放锁,而cpu在转换线程阻塞的时候会引起上下文切换,从而导致效率低
    • ree :非阻塞同步:基于冲突检测的乐观并发策略。
      - 如果没有线程争用共享数据,操作成功。否则,产生冲突就再进行补偿措施(不断的重试,直到成功)

乐观的并发策略里,需要操作和冲突检测具备原子性,这里用到了CAS操作Compare and Swap ,ReentrantLock中实现非公平锁的时候用到了compareAndSetState,compareAndSet()叫做非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他的线程

3.ReadWriteLock

// 是一个接口  只定义了两个方法
//读和写分成两个锁,从而实现多个线程可以同时进行读操作
public interface ReadWriteLock {
   
    Lock readLock();

    Lock writeLock();
}
  1. ReentrenReadWriteLock
//并为实现Lock接口  只实现了ReadWriteLock接口
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
            
        }

多个线程进行【读】操作

public class RwLockTest {
    //创建锁
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void read(Thread thread) {
        //获取锁
        rwLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            System.out.println("线程" + thread.getName() + "read start");
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println("线程" + thread.getName() + "read ing ");
            }
            System.out.println("线程" + thread.getName() + "read end");
        } finally {
            //必须释放锁
            rwLock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        //必须用final  不然下面两个线程会创建两个对象副本
        final RwLockTest rwtEST = new RwLockTest();
        new Thread("AAA") {
            @Override
            public void run() {
                rwtEST.read(Thread.currentThread());
            }
        }.start();

        new Thread("BBB") {
            @Override
            public void run() {
                rwtEST.read(Thread.currentThread());
            }
        }.start();
        
    }
}

打印结果:
线程BBB  read start
线程AAA  read start
线程BBB  read ing 
线程AAA  read ing 
……
线程BBB  read ing 
线程AAA  read ing 
线程BBB  read ing 
线程AAA  read end
线程BBB  read end

//可以看到两个线程是同时读的 

【注意】

  • 如果一个线程已经占用了读锁,其他线程要申请写锁,该线程会一直等待释放读锁
  • 如果一个线程占用了写锁,此时申请读或者写的线程,都要等待释放写锁
  1. 锁的概念
  • 可重入锁
    • 如果锁具备可重入性,成为可重入锁
    • 表明锁的分配粒度:基于线程的分配,而不是基于方法的分配
    • 例子:
//method1、method2都用synchronized修饰,当一个线程获取到method1的锁,而需要调用method2时,不需要再次申请锁,可以直接访问。
//假设不具备可重入性,该线程获取到该对象的锁,又要申请该对象的锁,会造成死锁情况。
class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}

你可能感兴趣的:(二、锁)