当使用synchronzied关键字修饰实例变量或者方法的时候,它会为临界资源上锁,只有访问的线程获得锁后才可以使用临界资源,在单核CPU上,并行算法不一定优于串行算法,因为单核CPU需要不停的在线程之间切换和调度,在并行算法中,合理的使用锁可以降低线程之前对锁的竞争导致的时间的浪费。
使用锁的建议:
⑴在同步方法中,减少锁的持有时间,最简单的是对于不需要同步的方法或者变量,我们不需要将其放在同步方法中,使其占用锁的占用时间,只对必要的方法使用synchronized,当然我们也可以在逻辑上进行处理;减少锁中临时变量的创建,使用全局变量等方法来减少线程在临界资源中的消耗;
package com.dong.Lock_Optimization;
/**
* 锁优化,减少锁的持有时间;
* @author liuD
*
*
*结果分析:(运行结果根据不同的电脑配置结果不一)
*当将锁放在addI()方法上时,它会对整个方法加锁,临界资源包括三个语句,完成操作的时间是:
*i=1000---> 1000 个线程 共花费373 Time ,注意: 每次运行的结果不一样,我的电脑最好是300多
*
*当减少不需要的临界资源,即将锁放在i++操作上时;临界资源只有一个语句,完成操作的时间是:
*i=1000---> 1000 个线程 共花费222 Time , 结果不一样,有300左右,最好222,
*
*/
public class TestReduceLockTime {
private static int i = 0;
public static synchronized void addI() {
//System.out.println(Thread.currentThread().getName()+ "准备添加");
synchronized (TestReduceLockTime.class) {
i++;
}
//System.out.println(Thread.currentThread().getName()+ "添加完毕");
}
public static void main(String[] args) throws InterruptedException {
ThreadObject to = new ThreadObject();
//获取自1970年1月1日0时起的毫秒数
long start =System.currentTimeMillis();
for(int i = 0 ;i < 1000 ;i++) {
Thread t = new Thread(to);
t.start();
t.join();
}
long end = System.currentTimeMillis();
System.out.println("i="+TestReduceLockTime.i+"---> 1000 个线程 共花费" + (end-start)+" Time");
}
}
class ThreadObject implements Runnable{
public void run() {
TestReduceLockTime.addI();
}
}
⑵缩小锁定对象的范围,从而减少锁冲突的可能性;减少锁的粒度重点在于使用最小的临界资源,减少线程之间竞争锁资源的冲突,当然使用最小临界资源,也就变相的减少了锁的持有时间;代码和上面同理,减少锁的范围;
⑶锁分离:对于同步资源,针对不同的操作,当各个操作对于同步资源可以不需要同步的时候,我们可以针对这些操作不设置同步限制,而对于需要同步的操作,我们通过加锁来设置其同步的执行,例如读写锁的分离,当没有写操作的时候,多个线程进行读时,是不需要同步的,只有涉及写操作的时候,才会对临界资源访问临界资源的读操作和写操作使用同步机制。
package com.dong.Lock_Optimization;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
* 测试读写锁的效率,即锁分离
Thread-990在1540881178608时刻点在设置i的新值3
Thread-992在1540881178608时刻点在设置i的新值9
Thread-993正在读取i的新值9
Thread-994在1540881178609时刻点在设置i的新值1
Thread-995正在读取i的新值1
Thread-996在1540881178609时刻点在设置i的新值1
Thread-997正在读取i的新值1
Thread-998在1540881178610时刻点在设置i的新值0
Thread-999正在读取i的新值0
执行所有读写线程共花费:336毫秒
运行5次: 387 499 455 457 507 336
* @author liuD
*/
public class TestLockSeparation {
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static WriteLock wlock = lock.writeLock();
static ReadLock rlock = lock.readLock();
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
Thread write = new Thread ( new writeThread());
Thread read = new Thread(new readThread());
write.start();
read.start();
read.join();
write.join();
}
long end = System.currentTimeMillis();
System.out.println("执行所有读写线程共花费:" + (end - start)+ "毫秒");
}
}
class Data{
static int i ;
public static int getI(){
System.out.println(Thread.currentThread().getName()+"正在读取i的新值"+ i);
return i;
}
public static void setI(int j) {
System.out.println(Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"时刻点在设置i的新值"+ j);
i = j;
}
}
class readThread implements Runnable{
public void run() {
TestLockSeparation.rlock.lock();
Data.getI();
TestLockSeparation.rlock.unlock();
}
}
class writeThread implements Runnable{
public void run() {
TestLockSeparation.wlock.lock();
Data.setI((int)(Math.random()*10));
TestLockSeparation.wlock.unlock();
}
}
package com.dong.Lock_Optimization;
/**
* 使用Synchronized关键字,对读写操作都是用锁
* Thread-993正在读取i的新值0
Thread-992在1540880987915时刻点在设置i的新值7
Thread-994在1540880987915时刻点在设置i的新值0
Thread-995正在读取i的新值0
Thread-996在1540880987916时刻点在设置i的新值0
Thread-997正在读取i的新值0
Thread-999正在读取i的新值0
Thread-998在1540880987917时刻点在设置i的新值9
执行所有读写线程共花费:338毫秒
运行了5次,440,313,439,304,338,
* @author liuD
*
*/
public class TestLockSeparation2 {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
Thread write = new Thread ( new newwriteThread());
Thread read = new Thread(new newreadThread());
write.start();
read.start();
read.join();
write.join();
}
long end = System.currentTimeMillis();
System.out.println("执行所有读写线程共花费:" + (end - start)+ "毫秒");
}
}
class newData{
static int i ;
public synchronized static int getI() {
System.out.println(Thread.currentThread().getName()+"正在读取i的新值"+ i);
return i;
}
public synchronized static void setI(int j) {
System.out.println(Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"时刻点在设置i的新值"+ j);
i = j;
}
}
class newreadThread implements Runnable{
public void run() {
Data.getI();
}
}
class newwriteThread implements Runnable{
public void run() {
Data.setI((int)(Math.random()*10));
}
}
有时候synchronized同步方法的效率高于读写锁,有时候低,一是synchronized在后期被官方优化,效率大大提升,其次,本次有关读的操纵中,没有太多耗时操作,如果读操作复杂,在读写锁的优势就会出来,因为读写锁中,读读操作不需要同步,而synchronized同步方法中,只要使用临界资源,都需要同步;
⑷锁粗化:对于同步资源,如果对同一锁的要求一直在进行,且是同一对象需要访问,如果按照原来的想法,同一个访问对象会一直存在于获取锁,释放锁的过程中,这样会浪费资源,所以对于同一对象的连续访问同一资源,我们可以将这些请求整合成一次请求,从而减少对锁请求同步次数。
//如果读完要立刻进行重新设值,上一个例子使用synchronized同步读,写,现在我们可以将读和写操作放在一起,只使用一个锁就可以,可以减少加锁和解锁的次数;
public synchronized static int getI() {
System.out.println(Thread.currentThread().getName()+"正在读取i的新值"+ i);
Data.setI((int)(Math.random()*10));
return i;
}
JVM对锁做出哪些优化:
⑴偏向锁:当一个线程获得锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需在做任何同步操作,适用场景是:同一个线程连续的获取锁,访问临界资源的情况下,可以避免不必要的加锁和释放锁,但是,当多个不同的线程竞争锁资源的时候,对于不同的线程,需要分别尽心加锁和释放锁,偏向锁的效果就一般般了。
⑵自旋锁:当线程没有获取锁的时候,他会进入等待队列,即在操作系统层面别挂起,但是挂起一个线程比较浪费时间,所以虚拟机做了一个优化,即假设线程马上就可以获取到锁,所以虚拟机会让当前线程做几个空循环,在几个循环后,如果获得锁,就进入临界资源,如果没有获得锁,则线程将在操作系统层面挂起;
⑶锁消除:JIT编译时,通过对运行上下文的扫描,将剔除不可能存在共享资源竞争的锁,因为在使用某些内置的API的时候,它的内部是使用锁机制的。
⑷轻量级锁:
简单的将对象头部作为指针,指向只有锁的线程堆栈的内部,来判断一个线程是否持有对象锁,如果线程获得轻量级锁成功,则可以顺利进入临界区,如果获取失败,则当前线程就会成为重量级锁;
无锁:
CAS(比较交换):不存在线程之间的调用和不存在锁竞争的系统开销。
cas算法过程:它包含是三个参数CAS(U,E,N)=》Compare And Swap(Update,Expected,New) ,U表示要更新的变量,E表示预期的值,N表示新值,当且仅当U == E时,才会将U的值设置为N,如果U != E ,则说明其他线程已经对该值做了更新,则当前线程什么都不做,最后,CAS返回当前U的真实值。
当多个线程使用CAS访问一个变量的时候,只有一个会成功的改变值,其他的都会失败,失败的线程要么继续使用CAS访问该变量,要么做其他事情,不存在锁和挂起;
最后:内容来自《Java高并发程序设计》 作者葛一鸣 郭超 由衷感谢作者为我们提供书籍内容;