同步代码块

public class Test{
    private Object o = new Object();
    private int count = 0;
     
    public void test1(){
        //其余耗时操作
        synchronized(o){
            //临界资源(多个线程都能访问的资源)
            count++;
        }
    }

当代码中有其余的耗时操作 而我们需要同步的代码又只占一小部分的时候 可以使用同步代码块

对于常见类型 可以使用其原子性对象 原子对象的方法都是原子操作,可以保证线程安全如

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

方法锁

public void test2(){
    synchronized(this){
        //锁当前对象
        //多个线程访问 [ 同一个对象 ] 时生效
    }
}

public synchronized void test3(){
    //锁当前对象  等同于上面
}

证明锁同一个对象

public class Test{
    public synchronized void methodA();
    
    public synchronized void methodB();
}

针对上面的代码

创建两个线程分别去调用同一个对象的methodA和methodB 会触发线程同步

以下情况不会触发线程同步

  1. 不同对象的同一方法
  2. 不同对象的不同方法
  3. 同一对象的同步和不同步方法

以下讨论方法锁的几种常见情况

public class Test{
    int temp = 0;
    public synchronized void setTemp(int temp){
        //复杂的逻辑代码........
        this.temp = temp;
    }
    
    public int getTemp(){
        return this.temp;
    }    
}

如上面代码 如果两个线程分别去调用同一个对象的setTemp和getTemp方法 会导致脏读 一般建议set 或get方法都设置为同步方法

public class Test{
    
    public synchronized test1(){
        test2();
    }
    
    public synchronized test2(){}
    
}

如上代码为锁的可重入,即同一个线程访问同一个对象多次调用同步方法时可重入,上面代码执行结果为test1()阻塞等待test2()执行结束才会继续执行

class ATest{
    public synchronized void test();
}

class BTest extends ATest{
    @Override
    public synchronized void test(){
        super.test();
    }
}

如上代码也是可重入的一种应用 如果去调用子类的test()方法 子类会阻塞等待父类执行完毕之后再继续执行

class ATest{
    int count = 0;
    public synchronized test(){
        while(true){
            count++;
            if(count % 5 == 0){
                count = count / 0;
            }
        }
    }
}

如上代码 如果创建两个线程去调用同一个对象的该方法,第一个线程抛出异常时会释放锁,第二个对象获得对象锁继续执行 在遇到同步业务逻辑的时候 抛出异常后该如何处理是需要考虑的

public class Test{
    Object o = new Object();
    
    public void test(){
        synchronized(o){
            while(true){
            	System.out.println("23333333");    
            }
        }
    }
}

如上代码 创建两个线程,第一个线程去执行test()方法 ,第二个线程执行之前先执行 o = new Object(),再去调用test()方法,并不会影响第一个线程 ,同步代码块一旦上锁之后,会有一个临时锁引用指向对象和真实的引用无关,在锁释放之前,修改锁对象引用,不会影响代码块的执行。

public class Test{
    String s1 = "lock";
    String s2 = "lock";
    
    public void test(){
        synchronized(s1){}
    }
    
    public void test2(){
        synchronized(s2){}
    }
}

如上代码 创建两个线程分别去调用同一个对象的test1()和test2()方法,第二个线程无法执行,因为test和test2锁的是同一个对象,所以在定义同步代码块的时候,不建议使用常量对象作为锁对象。

类锁

 public static synchronized void test4(){
 //锁class对象
 }

public static void test5(){
    synchronized(this.class){
    //锁class类对象 等同于上面
    }
}

两个线程去访问同一不同一对象的methodA和methodB都会触发线程同步

Volatitle

public class Test{
    boolean flag = true;
    public void test(){
        while(flag){}
    } 
}

对于上面的代码 如果一个线程去调用test()方法 ,然后将flag改为false 不能结束第一个线程的运行,这是由于cpu在读取代码的时候,会将临时变量值存储到缓存之中,当cpu中断或某些情况发生时,cpu才会清空缓存,重新读取内存临时变量值到缓存中。解决这种问题可以使用volatitle来修饰flag通知cpu将缓存和内存里面的临时变量的值同步,这也称为内存的可见性。

public class Test{
    volatitle int count = 0;
    public void test(){
        for(int i = 0; i < 10000; i++){
            count++;
        }
    }
}

如上代码如果创建多个线程去执行test方法,并通过join连接起来,最终打印count会发现count离预想的值要差很多,因为volatitle只能保证可见性,但并不能保证原子性。

CountDownLatch

public class Test{
    CountDownLatch latch = new CountDownLatch(5);//创建5个门闩
    
    public void test1(){
        try{
            latch.await();//等待门闩解开
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("strat----->");
    }
    
    
    public void test2(){
        for(int i = 0; i < 10; i++){
            if(latch.getCount() != 0){
                System.out.println(latch.countDown());//当前门闩的锁数
                latch.countDown();//减门闩上的锁
            }
        }
    }
}

可以和锁混合使用,或者替代锁的功能,在门闩未完全开放之前等待,在门闩开放之后执行,避免锁的效率低下问题。

ReentrantLock

建议使用的同步方式,相对效率比synchronized高,量级较轻。

一定要手动释放锁标记,一般都是放到finally里面

public class Test{
    Lock lock = new ReentrantLock();
    
    public void test1(){
    	try{
            lock.lock();
            System.out.println("---------------->");
        }finally{
            lock.unlock();
        }    
    }
    
    public void test2(){
        lock.lock();
        System.out.println("------------------->");
        lock.unlock();
    }
    
}

ReentrantLock在lock()加锁之后 到unlock()之间的代码都属于同步代码

//尝试获得锁
public class Test{
    Lock lock = new ReentrantLock();
    
    public void test1(){
        try{
            lock.lock();
            System.out.println("--------->");
        }finally{
            lock.unlock();
        }
    }
    
    public void test2(){
        boolean isLock = false;
        try{
            //尝试获得锁 如果获得成功返回true
            //获取不成功 返回 false
            //isLock = lock.tryLock();
            
            
            //阻塞尝试锁 在设置的时常内尝试获取锁标记 
            //如果超时 则直接返回false 不等待
            isLock = lock.tryLock(5,TimeUnit.SECONDS);
            if(isLock){
                System.out.println("已上锁");
            }else{
                System.out.println("未上锁");
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(isLock){
                //一定要先判断是否获取到锁标记 然后去开锁
                //否则会抛出异常
                lock.unlock();
            }
        }
    }
}

尝试获得锁用来判断某个线程是否获得了锁对象

//尝试打断锁
public class Test{
    Lock lock = new ReentrantLock();
    
    public void test1(){
        try{
            lock.lock();
            System.out.println("--------->");
        }finally{
            lock.unlock();
        }
    }
    
    public void test2(){
        try{
            lock.lockInterruptibly();//可尝试打断  阻塞等待锁 可以被其他线程打断阻塞状态
        }catch(Exception e){
            System.out.println("test2 interruptibly.....");
        }finally{
            try{
                lock.unlock();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args){
        //第一个线程去执行test1()
        //第二个线程去执行test2()
        t2.interrupt();
    }
}

如上代码 一个线程正在执行过程中, 另外一个线程尝试去获取锁进入阻塞状态,这种阻塞状态可以被外界打断,并抛出异常,类似于睡觉被别人突然叫醒。

阻塞状态又分为 普通阻塞、阻塞队列、锁池状态

普通阻塞 sleep() 可以调用thread.interrupt()被打断,并抛出异常

阻塞队列 调用wait()进入阻塞状态,只能被唤醒,无法被打断

锁池队列 无法获取锁对象,并不是所有的所持状态的线程都可以被打断

    使用ReentrantLock的lock()获取锁标记,如需阻塞等待锁标记,则无法被打断

    使用ReentrantLock的lockInterruptibly()获取锁标记,如需阻塞等待锁标记,则可以打断

//公平锁
public class TestReentrantLock extends Thread{
    private ReentrantLock lock = new ReentrantLock(true);
    @Ovrride
    public void run(){
        for(int i = 0; i < 5; i++){
            lock.lock();
            System.out.println("--------->");
            lock.unlock();
        }
    }
}

如上代码 创建两个线程去执行run(),当第一个线程在执行第一次循环的时候,第二个线程处于阻塞状态,第一个线程执行完第一次循环进入阻塞状态,第二个线程执行第一次循环,会出现两个线程交替执行的情况

在os中线程竞争锁是不公平的,当三个线程同时阻塞等待一把锁,锁释放的时候,不一定是按照先来后到的顺序去获得锁,也许是等待时间较短的先获得锁,而公平锁则是为解决这种问题出现的,等待时间越长一定会获得锁。其底层使用队列实现。

//生产者消费者(ReentrantLock)
public class Test{
    private final LinkedList<E> list = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;

    private ReentrantLock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();
    
    public int getCount(){
        return this.count;
    };
    
    
    public void put(E e){
        lock.lock();
        try{
        	while(list.size() == MAX){
			producer.await();
        	}    
            list.add(e);
            count++;
            consumer.singalAll();
        }finally{
            lock.unlock();
        }
    }
    
    public void get(){
        E e = null;
        lock.lock();
        try{
            if(list.size() == 0){
                cusumer.await();
            }
            e = list.removeFist();
            count--;
            producer.singalAll();
        }finally{
            lock.unlock();
        }
        return e;
    }
    
}

如上代码是生产者消费者简单实现,通过设置Condition条件来控制线程之间协作,通过singalAll去唤醒对应的线程,不同于使用notifyAll(),singalAll()能唤醒线程的更精准。

锁的底层原理

对象在堆内存中存储主要分为三部分

对象头 存储对象的hashCode、锁信息、分代年龄、GC标志、类型指针指向对象的类、元数据

实例变量 存放对象的实例变量包括父类的实例变量

填充数据 jvm要求对象的起始位置必须为整数倍,填充数据不是必须存在的,仅仅是为了字节对齐

在对象中加锁,数据存储在对象头信息中,在执行synchronized同步方法或同步代码块的时候,会在对象头部记录锁标记,锁标记指向monitor对象(也称为管程或对象监视器锁)的起始位置,每个对象都存在与之对应的monitor对象存在,monitor可以在对象创建时产生,也可以在线程执行同步代码块的时候创建。一个线程持有了对象的锁,其实是线程与对象的monitor相关联。

在jvm中monitor是由ObjectMonitor实现的

在Monitor中存在_WaitSet (管理等待队列) _EntryList ( 管理锁池阻塞线程 ) _Owner ( 记录当前执行的线程 ) 三种标记, 线程的状态如下图所示

当多个线程访问同一个同步代码的时候,线程先进入锁池_EntryList中,当某个线程获取锁标记的时候,Monitor的 _Owner会记录该线程,并在Monitor的计数器中执行递增运算(+1),当该线程执行完毕或wait()放弃锁的时候,Monitor计数器置为0,同时 _Owner置为null,等待下一个线程获取锁标记。

锁的种类

java中的锁大致分为偏向锁、轻量锁、重量锁、自旋锁。

锁的升级方式为 系统先提供偏向锁,偏向锁不能满足需求则提供轻量锁,轻量锁不满足要求则提供重量锁,自旋锁是一种锁的状态,不是一种实际的锁的类型。

锁的升级由jvm操作无需程序员干涉。

偏向锁 当线程中不可能存在多个线程争夺一个锁的情况下,jvm解释执行的时候会自动放弃同步信息,消除synchronized同步代码的结果,使用锁标记的形式记录锁状态,在Monitor中使用 ACC_SYNCHRONIZED 记录获取对象的线程,可以避免锁的抢夺和锁池状态的维护。


轻量级锁 当出现多个线程争夺同一个对象锁的时候,先提升为轻量级锁,使用 ACC_SYNCHRONIZED 标记获得锁的线程,使用 ACC_UNSYNCHRONIZED 标记未获得锁的线程,两个线程争夺同一个对象锁的时候可能会出现轻量级锁。


重量级锁 在锁的底层原理中解释的即为重量级锁


自旋锁 是一个过度锁,是偏向锁和重量锁的过渡,在获取锁的过程中,未获取到,jvm为了效率,会自动进行多次空循环,来保持线程的状态,而不是进入阻塞,自旋锁避免了线程状态的变更

Class BuyTicks implements Runnable{
    
    private int ticks = 10;
    boolean flag = true;
    @Override
    public void run(){
        if(flag){
            buy();
        }
    }
    
    public void buy(){
        if(ticks <= 0){
            flag = false;
            return;
        }
        
        System.out.println("get ticks " + ticks--);	
    }
}
class Account {
    private int money ;
    private String name;
    public Account(int money,String name){
        this.money = money;
        this.name = name;
    }
}


class persion implements Runnable{
    private Account account;
    private int nowMoney;
    private int drawingMoney;
    public Account(Account account,int nowMoney,int drawingMoney){
        this.account = account;
        this.nowMoney = nowMoney;
        this.drawingMoney = drawingMoney;
    }
    @Override
    private void run(){
        if(account.money - drawingMoney <= 0){
            
        }
        
        account.money = account.money - drawingMoney;
        nowMoney = nowMoney + drawingMoney;
        System.out.println("account money [" + account.money + "]");
        System.out.println("now money [" + nowMoney + "]");
    }
}