一、volatile关键字
volatile关键字的目的是告诉虚拟机:
1.每次访问变量时,总是获取主内存的最新值;
2.每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
volatile关键字解决了共享变量在线程间的可见性问题。
二、守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
Thread t = new MyThread();
t.setDaemon(true);
t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
三、同步
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
四、synchronized的使用
* 我们来概括一下如何使用synchronized:
* 找出修改共享变量的线程代码块;
* 选择一个共享实例作为锁;
* 使用synchronized(lockObject) { ... }
五、可重入锁
* Java的线程锁是可重入的锁。
* 对同一个线程,能否在获取到锁以后继续获取同一个锁?
* 答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
* 由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。
* 每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
* Java的synchronized锁是可重入锁;
* 死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
* 避免死锁的方法是多线程获取锁的顺序要一致。
六、wait和notify的使用
* wait()方法的执行机制非常复杂。
* 首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。
* 其次wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
* 因此,只能在锁对象上调用wait()方法。
* 必须在已获得的所对象上调用notify()或者notifyAll()方法。
* 已唤醒的线程还需要重新获得锁后才能继续执行。
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
七、使用ReentrantLock
/**
* java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
* 因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,
* 而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。
* ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。
* 下述代码在尝试获取锁的时候,最多等待5秒。如果5秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。
* 所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
* 必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁;
* 可以使用tryLock()尝试获取锁
*/
class TestReentrantLock{
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n){
try {
if(lock.tryLock(5, TimeUnit.SECONDS)){//5秒内尝试去获取锁
count += n;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放锁
}
}
}
八、使用Condition
/**
* ReentrantLock使用Condition对象来实现wait和notify的功能
* 使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例
* Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的
* await()会释放当前锁,进入等待状态
* signal()会唤醒某个等待线程;
* signalAll()会唤醒所有等待线程;
* 唤醒线程从await()返回后需要重新获得锁。
*/
class TestCondition{
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String task){
lock.lock();
try {
queue.add(task);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
condition.await();
/*if(condition.await(1, TimeUnit.SECONDS)){
//被其他线程唤醒
}else{
//指定时间内没有被其他线程唤醒
}*/
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
九、使用ReadWriteLock
/**
* 读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock
* ReentrantReadWriteLock--是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock
* ReadWriteLock只允许一个线程写入(其他线程既不能写入也不能读取)
* ReadWriteLock允许多个线程在没有写入时同时读取(提高性能)
* ReadWriteLock适合读多写少的场景
*/
class TestReadWriteLock{
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private Lock rlock = rwlock.readLock();
private Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void increase(int index){
wlock.lock();//加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock();//释放写锁
}
}
public int[] get(){
rlock.lock();//加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock();//释放读锁
}
}
}
十、使用StampedLock
/**
* StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
* 乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。
* 悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。
* 显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
* StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
* StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁
*/
class TestStampedLock{
private final StampedLock stampedLock = new StampedLock();
private Double x;
private Double y;
public void move(Double x1,Double y1){
Long stamp = stampedLock.writeLock();// 获取写锁
try {
x += x1;
y += y1;
} finally {
stampedLock.unlockWrite(stamp);//释放写锁
}
}
public Double distanceFromOrigin(){
//获取一个乐观读锁
long stamp = stampedLock.tryOptimisticRead();//有竞争返回0
Double currentX = x;
Double currentY = y;
//如果在发行给定的戳记时未独占获取锁,则返回true。
//如果stamp为零,则始终返回false。
//如果戳记表示正确持有的锁,则始终返回true。
if(!stampedLock.validate(stamp)){
stamp = stampedLock.readLock();//获取悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp);//释放悲观读锁
}
}
//返回正确舍入的双精度值的正平方根
return Math.sqrt(currentX*currentX + currentY*currentY);
}
}
十一、使用线程池
/**
* JDK提供了ExecutorService实现了线程池功能:
* 线程池内部维护一组线程,可以高效执行大量小任务;
* Executors提供了静态方法创建不同类型的ExecutorService;
* 必须调用shutdown()关闭ExecutorService;
* ScheduledThreadPool可以定期调度多个任务。
*/
class TestThreadPool{
public static void main(String[] args) {
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
};
Runnable stask1 = new Runnable() {
@Override
public void run() {
System.out.println("定时输出:任务1");
}
};
ExecutorService executor1 = Executors.newFixedThreadPool(5);//定长线程池
executor1.submit(task1);
executor1.shutdown();
ExecutorService executor2 = Executors.newSingleThreadExecutor();//单线程池
executor2.submit(task1);
executor2.shutdown();
ExecutorService executor3 = Executors.newCachedThreadPool();//单线程池
executor3.submit(task1);
executor3.shutdown();
//还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
// 2秒后开始执行定时任务,每3秒执行一次:
ses.scheduleAtFixedRate(stask1, 2, 3, TimeUnit.SECONDS);
//创建指定动态范围的线程池
int corePoolSize = 5;//核心线程池大小
int maximumPoolSize = 10;//最大线程池大小
long keepAliveTime = 60L;//线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
TimeUnit unit = TimeUnit.SECONDS;//keepAliveTime时间单位
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(10);//阻塞任务队列
ExecutorService es = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);//线程池的大小限制在5~10个之间动态调整
ExecutorService es0 = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());//动态创建
/**
* 1、构造一个固定线程数目的线程池,配置的corePoolSize与maximumPoolSize大小相同,同时使用了一个无界LinkedBlockingQueue存放阻塞任务,因此多余的任务将存在再阻塞队列,不会由RejectedExecutionHandler处理
*/
ExecutorService es1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
/**
* 2、构造一个缓冲功能的线程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一个无容量的阻塞队列 SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁
*/
ExecutorService es2 = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
/**
* 3、构造一个只支持一个线程的线程池,配置corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;保证任务由一个线程串行执行
*/
ExecutorService es3 = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
}
十二、使用ThreadLocal
在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context)。
Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
特别注意ThreadLocal一定要在finally中清除:因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。使用ThreadLocal要用try ... finally结构,并在finally中清除。