同步代码块
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 会触发线程同步
以下情况不会触发线程同步
- 不同对象的同一方法
- 不同对象的不同方法
- 同一对象的同步和不同步方法
以下讨论方法锁的几种常见情况
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 + "]");
}
}