java多线程系列:
前言
前一章简单介绍了Thread和Runnable,本章将重点介绍线程同步
多线程的问题
大家都知道多线程可以充分利用计算机资源,提高程序效率,但是稍不留神就会出现难以察觉的问题,而这些问题多数与数据竞争、竞态条件、缓存变量有关。
-
竞态条件
竞态条件常见的就是先检查后执行(check-than-act)假定n和m是实例变量,一个线程在执行完n==8.0的判断后,被调度器暂停了,另外一个线程获取了执行时间片段,修改了n的值;这时重新获取执行权后m的值不会等于4.0了
if(n==8.0){ m=n / 2.0 }
-
数据竞争
指两个或两个以上的线程并发的访问同一块内存区域,同时至少有一个线程在执行写操作,导致没有获取到预期的结果。通俗理解并发下值未同步,产生多写误差。假定线程1调set(10),线程2调get()本应该获取线程1修改后的值,实际没有获取到,这时产生了数据竞争。解决这个问题只需要volatile关键就可以解决,后面讲解volatile
public class demo{ private int a; public void set(int a){ this.a = a; } public int get(){ return a; } }
-
缓存变量
字面意思是把变量缓存起来,实际是JVM以及操作系统为了提升性能,把变量先放到寄存器或者cpu的缓存区中,对应的每条线程都会有自己的变量拷贝,操作变量时也是操作自己的拷贝,其他线程不太可能看到自己拷贝的变量发生变化。
java如何规避这些问题,是本文重点要说的
synchronized
作用:
- synchronized旨在保证多个并发的线程不会同时执行同一块临界区,线程间是互斥执行临界区
- 同时还表现为可见性,线程在进入临界区时,会从主存中读出这些变量,离开时在把这些变量写入主存。当出现异常时会自动释放锁
用法:
- 修饰对象方法
- 修饰类方法
- 代码块
synchronized是怎么做到线程间互斥执行临界区的呢?是通过监视器来实现,后面会重点讲解监视器
同步方法
在方法的头部包含synchronized关键字。
在实例方法上,锁会和该方法的实例对象关联
public class demo{
private int num;
public synchronized int getNum(){
return num++;
}
}
在类方法上,锁会和该方法的类关联
public class demo{
private static int num;
public stataic synchronized int getNum(){
return num++;
}
}
同步块
常用的单例模式就是用的同步块,锁会和obj对象关联
public class SingletonDemo{
private SingletonDemo(){}
private volatile static SingletonDemo singleton;
private Object obj = new Object();
public static SingletonDemo newInstance(){
if(singleton == null){
synchronized(obj){
if(singleton == null){
singleton = new SingletonDemo();
}
}
}
}
return singleton;
}
锁
在jdk1.5之前java程序主要靠synchronized关键字实现锁的功能,jdk1.5之后java新增了一类实现Lock接口的锁,来实现synchronized类似的功能。相比synchronized,锁需要显示地获取和释放,在实际使用中也比synchronized相对灵活
synchronized&lock区别
tips | synchronized | Lock |
---|---|---|
实现 | JVM控制锁的获取和释放 | 基于JDK层面实现 |
使用 | 不需要手动释放锁 | 需要手动获取锁和释放锁(finally中unlock) |
锁获取超时 | 不支持,拿不到锁就一直在那等着 | 支持,可以设置超时时间,时间过了没拿到就放弃 |
获取锁响应中断 | 不支持 | 支持,可以设置是否可以被打断 |
释放锁的条件 | 满足一个即可:①占有锁的线程执行完毕 ②占有锁的线程异常退出 ③占有锁的线程进入waiting状态释放锁 | 调用unlock()方法 |
公平与否 | 非公平锁(公平指的是哪个线程等的时间长就把锁交给谁) | 默认为非公平锁,可以设置为公平锁(排队等候 |
public interface Lock {
//获取锁
void lock();
//获取可中断的锁
void lockInterruptibly() throws InterruptedException;
//仅当锁在调用时处于空闲状态时才获取锁,true:获取锁,false:未获取到锁
boolean tryLock();
//如果在给定的等待时间内是空闲的,则获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
//获取等待通知的组件
Condition newCondition();
}
ReentrantLock(重入锁)
重入锁可以实现synchronized的同样效果,并且比synchronized灵活
lock&unlock
public class ReentrantLockDemo {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> m1(), "A").start();
new Thread(() -> m1(), "B").start();
}
public static void m1() {
try {
lock.lock(); //加锁
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
tryLock&unLock
尝试锁定,不管是否锁定代码都将继续执行,实际我们可以根据锁定结果做自己的业务逻辑处理
public class ReentrantTryLockDemo {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> m1(), "A").start();
new Thread(() -> m1(), "B").start();
}
public static void m1() {
//是否获取到了锁
boolean isLock = lock.tryLock();
try {
if (isLock) {
//业务逻辑
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
}
else {
System.out.println("线程" + Thread.currentThread().getName() + "未获取到锁");
}
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
if (isLock) {
lock.unlock();
}
}
}
}
Fair&Nonfair(公平锁与非公平锁)
公平锁根据线程等待时间长短,来获取锁,等待时间长的可以优先获取锁执行,在性能上非公平锁大于公平锁
我们可以看到通过构造函数来创建公平和非公平锁
- RentrantLock 有三个内部类 Sync、NonfairSync 和 FairSync 类
- Sync 继承 AbstractQueuedSynchronizer 抽象类,也就是大名鼎鼎的AQS
- NonfairSync(非公平锁) 继承 Sync
- FairSync(公平锁) 继承 Sync
//公平锁
public class ReentrantFairLockDemo {
//true:公平锁
private static Lock lock = new ReentrantLock(true);
public static void main(String[] args) {
new Thread(() -> m1(), "A").start();
new Thread(() -> m1(), "B").start();
new Thread(() -> m1(), "C").start();
}
public static void m1() {
for (int i = 0; i < 6; i++) {
lock.lock(); //加锁
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
}
公平锁
new ReentrantLock(true)时表示公平锁, 线程是交替执行
非公平锁
非公平锁那就随机的获取,谁运气好,cpu时间片轮到哪个线程,哪个线程就能获取锁,和上面公平锁的区别很简单,就在于先new一个ReentrantLock的时候参数为false,当然我们也可以不写,默认就是false
Conditionde
使用Lock和Condition来实现生产者和消费者的同步容器,相比使用wait/notifyAll,使用Conditionde的方式能更加精确地指定哪些线程被唤醒
public class ProducerConsumerLockCondition<T> {
final private LinkedList<T> list = new LinkedList<>();
final private int MAX = 20;
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void set(T t) {
try {
lock.lock();
while (list.size() == MAX) {
producer.await();
}
list.add(t);
++count;
consumer.signalAll(); //通知消费者进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while (count == 0) {
consumer.await();
}
t = list.removeFirst();
count--;
producer.signalAll(); //通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
}
总结:
本文主要是简单介绍了编写java多线程程序可能会出现的问题,针对这些问题JDK为我们提供了丰富多样的实现方式。本文介绍的只是其中的冰山一角。后面会逐步介绍内部实现的原理。
转载:https://blog.csdn.net/wangjingjava/article/details/104533893