Java当中的锁都是为了保证多线程同步执行。如果没有锁的话,多线程是异步执行的。
什么是多线程同步?
请看下面的代码:
public class SynTest {
public static void main(String[] args){
Thread t1 = new Thread(){
@Override
public void run(){
testsync();
}
};
t1.setName("t1");
Thread t2 = new Thread(){
@Override
public void run(){
testsync();
}
};
t2.setName("t2");
t1.start();
t2.start();
}
public static void testsync(){
System.out.print(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
t1
t2
或者
t2
t1
可以看到线程t1和t2基本上是同时打印出来的。为了让线程按顺序一前一后执行,我们就可以给方法加锁。
public class SynTest {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(){
@Override
public void run(){
testsync();
}
};
t1.setName("t1");
Thread t2 = new Thread(){
@Override
public void run(){
testsync();
}
};
t2.setName("t2");
t1.start();
t2.start();
}
public static void testsync(){
reentrantLock.lock();
System.out.print(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
此次输出就是有间隔地输出,也就是第一个线程执行完释放锁之后第二个线程才开始执行。
Java当中有了synchronized关键字为啥还要用ReentrantLock呢?JDK1.6之前,synchronized关键字实现同步是一个重量级的锁,因为会去调用OS的函数(native方法),所以synchronized关键字没有源码,它都是用c++实现的。
比如当t1拿到锁执行synchronized中的同步代码时,线程t2此时尝试去执行synchronized中的同步代码,此时synchronized会去调用OS函数,CPU就需要切换成内核态,此时这个OS函数会进入阻塞状态。t1执行完释放锁之后t2拿到锁需要继续往下执行同步代码的话,CPU又要切换成用户态。
有个人看不惯这种同步方法,所以就开发了一个包解决同步技术。ReentrantLock就是这个包里面的一个同步技术。这个锁比synchronized关键字还快。
sun公司这下也坐不住了,所以在JDK1.7做了优化,synchronized虽然也会触发到内核,但是会让同步在JVM层面解决而不是在OS层面解决,优化成偏向锁、轻量锁和重量锁。
多线程同步内部是如何实现同步的
wait/notify,synchronized,ReentrantLock
模拟一些同步的思路
(1)自旋
所谓自旋锁就是循环循环自己不断循环。
// 标识---是否有线程在同步块---是否有线程上锁成功
volatile int status = 0;
void lock(){
// 不断循环while 直到拿到锁才跳出while循环
while (!compareAndSet(0,1)){
}
//lock
}
void unlock(){
status = 0;
}
boolean compareAndSet(int except,int newValue){
// cas操作,修改status成功则返回True
}
缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另一个线程就会白白地花费Ns的cpu资源。
改进思路:让得不到锁的线程让出cpu
(2)yield+自旋
volatile int status = 0;
void lock(){
while (!compareAndSet(0,1)){
yield();// 自己实现
}
//lock
}
void unlock(){
status = 0;
}
要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁定的时候把cpu给让出来,yield()方法就能让出cpu资源,当线程竞争锁失败时,会调用yield方法让出cpu。自旋锁+yield的方法并没有完全解决问题,当系统只有两个线程竞争锁时,yield是有效的。需要注意的是该方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程。
(3)sleep+自旋
volatile int status = 0;
void lock(){
while (!compareAndSet(0,1)){
sleep(10);
}
//lock-------------5minute
}
void unlock(){
status = 0;
}
sleep的时间为什么是10?怎么控制呢?就是你是调用者其实很多时候你也不知道这个时间怎么确定。
比如线程t1拿到锁执行5minute,但是没拿到锁的线程t2只是睡眠10s,也就是每10s就去看一下可不可以拿到锁。而假设t1只执行了1s,但是t2睡了10s,那就足足浪费了9s。
(4)park+自旋
public class ParkTest {
public static void main(String[] args){
Thread t1 = new Thread(){
@Override
public void run(){
testsync();
}
};
t1.setName("t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("-----------main");
// 叫醒t1
LockSupport.unpark(t1);
}
public static void testsync(){
System.out.print("t1 -1");
// 线程t1执行到park()时就会一直睡眠,需要被叫醒
LockSupport.park();
System.out.print("t1 -2");
}
}
输出:
t1 -1
-----------main
t1 -2
t1执行testsync()方法时,首先"t1 -1",然后就沉睡但是此时main()方法是继续往下执行的,所以打印了"-----------main",然后执行unpark()方法叫醒t1,t1醒过来之后就打印"t1 -2"。
park()方法并不是由LockSupport类提供的,LockSupport仅仅对park()方法做了封装,park()方法是由unsafe类提供的。
park()的实现机制如下:
volatile int status = 0;
Queue parkQueue; //集合 数组 list
void lock(){
while (!compareAndSet(0,1)){
park();
}
//lock
....
unlock();
}
void unlock(){
status = 0;
lock_notify();
}
void park(){
// 将当前线程加入到等待队列
parkQueue.add(currentThread);
// 将当前线程释放cpu,被释放cpu的线程执行到这一步就不再往下执行了,不会继续执行while
releaseCpu()
}
void lock_notify(){
// 得到要唤醒的线程头部线程
Thread t = parkQueue.header();
// 唤醒等待线程
unpark(t)
}
ReentrantLock其实就是基于这个机制实现的。
网友评论