共享问题
当多个线程同时对一块共享资源操作时就会发生共享安全问题
synchronized语法
Thread t1 = new Thread(()->{ synchronized(object){ //临界区 }).start()
临界区就是一段对共享资源访问的代码
如果把synchronized加载方法上 ,相当于给this对象加锁
如果加载static 方法上 相当于给类.Class对象加锁
局部变量
局部变量正常是没有安全问题的,因为局部变量存在栈帧中。栈帧在虚拟机栈中,每一个线程都会创建一个虚拟机栈所以不会存在线程安全问题,但是局部变量引用如果被外界的线程使用了就可能存在线程安全问题
比如一个子类重写了父类的方法,并在方法中创建了一个新的线程访问父类中的变量 。
可以给父类的方法上加final 不让子类影响
String 类就是这样做的 在类上加了final
常见线程安全类
这里组合调用
HashTable table = new HashTable(); Thread t1 = new Thread(()->{ if(table.get(k)==null){ table.put(k,value); } }).start ; Thread t2= new Thread(()->{ if(table.get(k)==null){ table.put(k,value); } }).start ;
组合调用是不安全的,可能第一个线程刚get完值,到第二个线程执行放入值,这时第一个线程还会放value进去,虽然每个方法是安全的,但是组合起来并不能保证其原子性,
monitor(监视器,管程,锁,都是指他)
每个java对象都会关联一个monitor 当线程获取到对象锁时就会把owner改为thread,这时当其他线程来获取锁时会进入EntryList阻塞等待,进入waiting的是调用了对象的wait()方法。 当owner中的thread执行完后,会让entryList中线程竞争得到锁,具体是谁取决于虚拟机实现,
字节码的角度
轻量级锁
当对象不存在竞争问题,多线程访问时间是错开的,会采用轻量级锁优化,
就是在线程中创建锁记录,将锁记录和对象的Markword交换,就视为已经上锁了,这时如果其他线程也来获得锁是不会成功的,会采取锁膨胀的方式,给这个对象申请monitor锁
如果没有,当线程结束后,会把markword 的内容和锁记录交换回来
如果发生锁重入的问题
在第二个锁的时候也会进行交换操作,但是会失败,所以他的那个锁记录就是null,当解除锁时,如果锁记录为null,就会去掉一个锁标记,如果不为null,代表最后一个锁了,所以会进行锁记录和markword交换。
自旋优化
就是当线程一得到了锁时,线程二在获取锁时,肯定获取不到,但是他不会立马进入阻塞,而是自旋多判断几次,如果几次都没获取到才会进入阻塞,但是如果获取到了,就减少了上下文切换带来的开销
对象头格式
偏向锁
正常在创建一个对象时偏向锁是开启的,
偏向锁是有延迟的,如果想立即生效,加VM参数 -xx:BiasedLockingStartupDelay=0
就是这个对象一直被一个线程所使用,偏向锁会在对象头上记录线程id每次获取锁时只会判断是不是自己的线程id,如果不是就会进行锁升级,升级成轻量级锁,
当调用对象的hashcode()方法时会 撤销偏向锁改为轻量级锁;,因为对象头上已经没有位置存放线程id只能存放对象的hashcode
当锁被撤销20次时会进行锁的批量重定向
当被撤销40次时,进行锁的升级,就是采取轻量级锁,新创建的对象也是轻量级锁
jvm优化
如果一个对象局部变量,在线程中被使用,且没有被外面使用,则在线程中的锁会被jit编译器视为没有,叫 逃逸分析。
api
wait 和notify
每个对象都有一个monitor,monitor中有一个waiting_set,当调用对象的wait方法
object.wait()线程就会进入monitor的waiting_set中等待,直到其他线程调用对象的notify方法 object.notify() 或者object.notifyAll();
调用wait的线程会释放锁。在调用之前必须持有锁。
notify会随机选择waiting_set中的一个线程唤醒。
notifyAll()会唤醒在waiting_set上等待的全部线程。
被唤醒的线程重新进入entryset 等待cpu调度。
waiting_set中可能存在不同条件的等待线程,如果有其中一个线程条件满足,使用notifyALl会唤醒全部线程,所以存在虚假唤醒问题,在条件外使用while循环
synchronized(lock){ while(条件不成立){ object.wait(); } }
wait和sleep
wait和sleep都会让线程进入waiting状态, wait是对象的方法,而sleep是Thread的方法
wait调用后会释放锁,让其他线程有机会拿到锁,而sleep并不会释放锁。
park unpark
LockSuport.park()
park会使线程暂停,unpark会使线程结束暂停
每个线程都会关联一个parker对象,其中包括condition,mutex,和counter
线程在调用park方法时会检查counter如果counter是0就会进入condition队列进行等待
如果counter是1则继续 运行,不需要暂停,并且把counter设置为0
如果提前调用unpark方法就会让counter变成1下次调用park方法时就不会暂停了
如果在park中的线程被interrupt了这时候会改变其打断标记,就算下次在调用park也不会暂停了需要调用线程的interrupted方法讲打断标记改为false则下次才可以继续调用park
死锁
如果线程T1先获取a锁再获取b锁,线程T2先获取b锁再获取a锁就可能导致双方都在等待对方释放锁,并且自己持有对方的锁,这就是死锁,
活锁
线程t1和t2都改变对方的循环终止的条件,就造成双方都无法执行结束,造成活锁,
可以通过改变sleep时间,让双方执行时间不同,来解决。
饥饿
线程长时间得不到cpu执行
ReentrantLock
可中断, 可重入,多条件等待, 可以公平锁,可以设置超时时间
可重入
ReentrantLock lock = new Reentrantlock() new Thread(()->{ lock.lock(); method1(); lock.unlock(); }).start(); public void method1(){ lock.lock(); lock.unlock(); }
可打断
ReentrantLock lock = new Reentrantlock() Thread t1= new Thread(()->{ try{ lock.lockInterruptibly(); }catch(Exception e){ e.printStackTrace(); return ; } lock.unlock(); }); lock.lock(); t1.start(); t1.interrupt();
锁超时
ReentrantLock lock = new Reentrantlock() Thread t1= new Thread(()->{ if(!lock.trylock()){ return; } lock.unlock(); });
trylock()可以解决死锁问题如果trylock第二层失败,则不会死等,会继续往下执行,执行lock.unlock()方法,释放已经拥有的锁
公平锁
ReentrantLock lock = new ReentrantLock(true);
条件变量
ReentrantLock lock = new ReentrantLock(); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Thread t1 = new Thread(()->{ lock.lock() c1.await(); c2.await(); c1.signal(); c2.signal(); lock.unlock(); })
和wait和notify用法一样。