前言: 这里只是我自己对于synchronized、Lock和ReadWriteLock的一个简单认识,想要学习一样东西,先有一个大概的认识,以后再慢慢深入学习相关的知识。所以,这里就只是一个代码的展示和一些个人的理解。
为了提高CPU的利用效率,引入了多线程。但是为了线程的安全问题,又回到了同步(单线程一定是同步的)。
这里使用一个简单的示例代码,来展示synchronized、Lock和ReadWriteLock的作用。首先提供一个简单的模型类:
抽象投票类
说明:一个抽象的模型类,用于投票观察者获取票数。
package learn;
public abstract class Voter {
public int vote; // 票数
public abstract int getVote(); // 获取票数
public abstract void setVote(int vote); // 设置票数
}
投票观察者类
说明:投票观察者类,多个线程同时观察同一个投票类,模拟多线程的同时读操作。
package learn;
public class VoteObserver implements Runnable {
private Voter voter;
public VoteObserver(String name, Voter voter) {
Thread.currentThread().setName(name);
this.voter = voter;
}
@Override
public void run() {
System.out.println("我是投票观察者:" + Thread.currentThread().getName() + " 现在有多少张票:" + voter.getVote());
}
}
不考虑线程同步的投票类
说明:没有考虑同步措施的投票类,这里我并不会真的使用多线程去修改投票数,我这里是模拟同时去读取票数,所以getVote()
方法会有一个耗时操作。
package learn;
public class UnsafeVoter extends Voter {
private int vote;
@Override
public int getVote() {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return vote;
}
@Override
public void setVote(int vote) {
this.vote = vote;
}
}
同步测试类
说明:启动了一个具有十个固定线程的线程池,然后添加任务进行执行,然后对线程任务完成进行计时(这里不考虑主线程的耗时)。
package learn;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 同步测试
* */
public class SyncTest {
public static void main(String[] args) {
unThreadSafe();
}
// 非线程安全方式,多个线程并发执行,速度最快,但是有线程安全问题
static void unThreadSafe() {
ExecutorService service = Executors.newFixedThreadPool(10);
Voter voter = new UnsafeVoter();
voter.setVote(100);
// 计时器
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
service.submit(new VoteObserver("observer_"+i, voter));
}
service.shutdown(); // 关闭线程池
while (!service.isTerminated()) {
} // 利用CPU空转计时,效率不高,但是简单易用。
System.out.println("总耗时:" + (System.currentTimeMillis()-start) + " ms");
}
}
测试结果
说明:由于没有进行同步操作,所以多个线程是并发获取票数的,即几乎是同时完成了任务,总耗时接近1000ms。因此使用多线程以后,多线程的代码变成了并行执行。
使用synchronized关键字的线程安全投票类
说明:使用synchronized进行同步操作,保证线程安全。上面的操作速度虽然快,但是无法保证线程安全,所以需要使用同步来保证,这里使用Java的synchronized关键字来完成。
package learn;
public class SafeVoterSync extends Voter {
private int vote;
@Override
synchronized public int getVote() {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return vote;
}
@Override
public void setVote(int vote) {
this.vote = vote;
}
}
使用synchronized关键字的测试方法
// 采用synchronized关键字进行同步的线程安全方式,多个线程同步执行,效率非常低下!
static void threadSafeSynchronized() {
ExecutorService service = Executors.newFixedThreadPool(10);
Voter voter = new SafeVoterSync();
voter.setVote(100);
// 计时器
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
service.submit(new VoteObserver("observer_"+i, voter));
}
service.shutdown(); // 关闭线程池
while (!service.isTerminated()) {
} // 利用CPU空转计时,效率不高,但是简单易用。
System.out.println("总耗时:" + (System.currentTimeMillis()-start) + " ms");
}
测试结果
说明:实际观察的话,可以发现每条记录是一次间隔接近1000ms的时间打印的,实际总耗时接近10*1000ms,因此同步以后,多线程的代码变成了串行执行。
使用Lock类的线程安全投票类
package learn;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeVoterLock extends Voter {
private static Lock lock = new ReentrantLock();
private int vote;
@Override
public int getVote() {
lock.lock();
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return vote;
}
@Override
public void setVote(int vote) {
this.vote = vote;
}
}
使用Lock类的测试方法
// 采用Lock关键字进行同步的线程安全方式,多个线程同步执行,效率非常低下!
static void threadSafeLock() {
ExecutorService service = Executors.newFixedThreadPool(10);
Voter voter = new SafeVoterLock();
voter.setVote(100);
// 计时器
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
service.submit(new VoteObserver("observer_"+i, voter));
}
service.shutdown(); // 关闭线程池
while (!service.isTerminated()) {
} // 利用CPU空转计时,效率不高,但是简单易用。
System.out.println("总耗时:" + (System.currentTimeMillis()-start) + " ms");
}
测试结果
说明:效果和synchronized是差不多的,听说java对synchronized进行了优化,现在性能和Lock也差不多了,但是我对这个的认识还比较浅显。
使用ReadWriteLock的线程安全投票类
说明:前面我们看到了,我们只是获取投票数这个读取的过程,如果加了多线程同步以后,代码的效率下降的非常严重,这个和日常生活也有点相似,任何事情一旦进入了排队等待,就感觉是一种煎熬——排队买票、排队领书、排队买奶茶等等。
但是,如果不会对状态进行修改,那就没必要进行同步。
package learn;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SafeVoterRWLock extends Voter {
/**
* 这里可以切换读写锁来测试,只使用读锁,我们就回到了那个并发执行的春天了,
* 使用写锁的话就和前面的同步没有区别了。所以,这也引出了它的应用场景,
* 即读多于写的场景下,效率很高,最坏情况下,基本同普通的Lock和synchronized一样。
* */
private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
private static Lock readLock = rwLock.readLock(); // 读锁
private static Lock writeLock = rwLock.writeLock(); // 写锁
private int vote;
@Override
public int getVote() {
readLock.lock(); // 加锁放在try块外部
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock(); // 注意上下两个都要修改为一样的,即加锁必须释放,否则就死锁了!
}
return vote;
}
@Override
public void setVote(int vote) {
this.vote = vote;
}
}
使用ReadWriteLock的测试方法
// 采用ReadWriteLock关键字进行同步的线程安全方式,多个读线程并发执行,效率非常高!
static void threadSafeRWLock() {
ExecutorService service = Executors.newFixedThreadPool(10);
Voter voter = new SafeVoterRWLock();
voter.setVote(100);
// 计时器
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
service.submit(new VoteObserver("observer_"+i, voter));
}
service.shutdown(); // 关闭线程池
while (!service.isTerminated()) {
} // 利用CPU空转计时,效率不高,但是简单易用。 注意,不能使用isShutDown方法。
System.out.println("总耗时:" + (System.currentTimeMillis()-start) + " ms");
}
/**
* synchronized代码块中的代码才是耗时的,其它代码都是并行执行的!
*
* 读写锁:
* 读读共享
* 读写互斥
* 写读互斥
* 写写互斥
* */
测试结果
多个读锁之间是共享的,即会并发执行。但是读锁和写锁、写锁和写锁还是需要互斥进行的,即会串行执行(这里只有读锁的并发执行)。
这里只是对synchronized、Lock和ReadWirteLock的进行一个使用,先掌握一个简单的用法,可能这里的场景不是很合适。但是,这反正也只是学习的第一步,先慢慢来吧。先写一个例子,记录一下自己学习的过程,多线程确实是一个难点,需要多想、多练、多看。