java -- 线程(二)
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争同步锁而产生的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁。
死锁的案例 : 同步代码块的嵌套
创建锁对象:
public class Lock {
public static final Lock lockA = new Lock();
public static final Lock lockB = new Lock();
}
测试类:
public class DeadLockTest {
public static void main(String[] args) {
while(true){
new Thread(new Runnable() {
@Override
public void run() {
synchronized (Lock.lockA){
System.out.println("getlockA...");
synchronized (Lock.lockB){
System.out.println("getlockB...");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (Lock.lockB){
System.out.println("getlockB...");
synchronized (Lock.lockA){
System.out.println("getlockA...");
}
}
}
}).start();
}
}
}
生产者与消费者
创建2个线程,一个线程表示生产者,另一个线程表示消费者
import java.util.ArrayList;
import java.util.List;
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
Object o = new Object();
// 生产者
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (o) {
if (list.size() > 0) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add("aaaa");
System.out.println(list);
// 唤醒消费线程
o.notify();
}
}
}
}).start();
// 消费者
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (o) {
if (list.size() == 0) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove(0);
System.out.println(list);
o.notify();
}
}
}
}).start();
}
}
线程方法sleep和wait的区别
- sleep()是Thread类静态方法,不需要对象锁。
- wait()方法是Object类的方法,被锁对象调用,而且只能出现在同步中。
- 执行sleep()方法的线程不会释放同步锁。
- 执行wait()方法的线程要释放同步锁,被唤醒后还需获取锁才能执行。
案例性能问题
wait()方法和notify()方法, 本地方法调用OS的功能,和操作系统交互,JVM找OS,把线程停止. 频繁等待与唤醒,导致JVM和OS交互的次数过多.
Condition接口
java.util.concurrent.locks.Condition
是一个接口类, 因此要使用其实现类创建对象
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象
以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)
其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用
// Condition常用方法:
public void await() // 线程等待
public void signal() // 唤醒一个等待的线程
public void singalAll() // 唤醒所有等待的线程
// 使用其实现类 ReentrantLock 的 newCondition方法获取 Condition
public Condition newCondition()
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 创建Lock
Lock l = new ReentrantLock();
// 获取 Condition 对象
Condition con = l.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
l.lock();
System.out.println("开始等待");
try {
con.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
l.unlock();
}
}
}).start();
Thread.sleep(2000);
System.out.println("准备唤醒");
l.lock();
con.signal();
l.unlock();
}
}
Condition接口方法和Object类方法比较
- Condition可以和任意的Lock组合,也就是实现了线程的分组管理。
- 一个线程的案例中,可以使用多个Lock锁,每个Lock锁上可以结合Condition对象
- synchronized同步中做不到线程分组管理
- Object类wait()和notify()都要和操作系统交互,并通知CPU挂起线程,唤醒线程,效率低。
- Condition接口方法await()不和操作系统交互,而是让线程释放锁,并存放到线程队列容器中,当被signal()唤醒后,从队列中出来,从新获取锁后在执行。
- 因此使用Lock和Condition的效率比Object要快很多
生产者和消费者案例改进
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OverWriteWakeUpWaiting {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Lock l = new ReentrantLock();
// 对线程进行分组管理
Condition con1 = l.newCondition(); // 生产线程, 对象监视器
Condition con2 = l.newCondition(); // 消费线程, 对象监视器
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
l.lock();
if (list.size() > 0) {
try {
con1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add("abc");
System.out.println(list);
con2.signal();
l.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
l.lock();
if (list.size() == 0) {
try {
con2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove(0);
System.out.println(list);
con1.signal();
l.unlock();
}
}
}).start();
}
}
java并发编程的三大特性
原子性
原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行
下面具有原子性的操作有?
x = 1;
// x = 1,是一个单纯的赋值操作,满足原子性。
y=x;
// 实际是两个操作,分别是 读取x变量 ,将x赋值给y,这两个操作分别来看都是原子性的,但是合起来就不是了
x++;
// 实际是三个操作 ,先读取变量 ,在进行+1操作 ,再赋值给x,不满足原子性
x=x+1;
// 同上,不满足原子性
JAVA提供了原子性的技术保障有如下:
1、synchronized (互斥锁)
2、Lock(互斥锁)
3、原子类(CAS)
synchronized 和 Lock 都是通过互斥锁实现,即同一时刻只允许一个线程操作该变量,保障了原子性
原子类AtomicInteger
/*
java.util.concurrent.atomic.AtomicInteger
构造方法
public AtomicInteger()创建具有初始值 0 的新 AtomicInteger。
public AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger。
方法
int incrementAndGet() 以原子方式将当前值加 1。
int getAndIncrement() 以原子方式将当前值加 1。
int decrementAndGet() 以原子方式将当前值减 1。
int getAndIncrement() 以原子方式将当前值减 1。
int getAndAdd(int delta) 以原子方式将给定值与当前值相加。
int addAndGet(int delta) 以原子方式将给定值与当前值相加。
int get() 获取当前值。
*/
public class Test02 {
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(1);
ai.incrementAndGet(); //++ai 2
ai.getAndIncrement(); //ai++ 3
System.out.println(ai.get());// 3
System.out.println(ai.getAndIncrement()); // 3
System.out.println(ai.get()); // 4
System.out.println(ai.incrementAndGet()); // 5
System.out.println(ai.get()); //5
}
}
CAS无锁机制
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。当多条线程尝试使用CAS同时更新同一个变量时,只有其中一条线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是告知这次竞争失败,并可以再次尝试.
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
-
循环时间长开销很大。
CAS 通常是配合无限循环一起使用的,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
-
只能保证一个变量的原子操作。
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
-
ABA问题。
第一条线程获取到V位置的值 假设是 1 第二条线程获取到V位置的值 也是1 第一条线程cas成功 将值改为 0 第一条线程又cas成功 将值改回 1 这时第二条线程cas 发现值没变 还是1 cas成功 实际上当第二条线程cas时 V位置的值已经从 1-0-1 这就是ABA问题 如何解决 每次获取V位置的值时,带上一个版本号.这样就可以避免ABA问题 java中AtomicStampedReference这个类在cas时就是通过版本号来解决的
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程应该能够立即看得到修改的值
public class Test {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1号线程启动....执行while循环");
long num = 0;
while(flag){
num++;
}
System.out.println(num);
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2号线程启动....修改flag的值为false,停止循环");
flag = false;
}
}).start();
}
}
通过如上案例 发现修改flag 的值并没有使循环结束
1.加锁,比如使用synchronized.
JMM关于synchronized的两条规定:
1)线程解锁前,必须把共享变量的最新值刷新到主内存中
2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值
public class Test {
//使用同步方法获取flag的值
public static synchronized boolean getFlag(){
return flag;
}
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1号线程启动....执行while循环");
long num = 0;
/*
线程调用getFlag方法时 先获取锁 也就是加锁
这时会先清空本地内存中共享副本的值,那么在使用值就需要从
主内存中重新获取 ,线程释放锁时,也就是解锁,会把共享变量flag
的值重新更新到主内存中
*/
while(getFlag()){
num++;
}
System.out.println(num);
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2号线程启动....修改flag的值为false,停止循环");
flag = false;
}
}).start();
}
}
2.使用volatile关键字保证可见性
public class Test {
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1号线程启动....执行while循环");
long num = 0;
while(flag){
num++;
}
System.out.println(num);
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2号线程启动....修改flag的值为false,停止循环");
flag = false;
}
}).start();
}
}
volatile缓存可见性实现原理
底层实现主要是通过汇编lock前缀指令,会锁住这块区域的缓存,并写回主内存.
1.会将当前处理器缓存的行数据立即写回系统内存
2.这个写回内存的操作导致CPU的缓存该内存地址的数值失效(MESI协议)
volatile只能保证可见性,但是不能保证原子性,如果要保证原子性,请使用锁
有序性
一般来说,程序的执行顺序按照代码的先后顺序执行.但是处理器为了提高程序的效率,可能会对代码的执行顺序进行优化,它不保证程序中各个语句的执行先后顺序一致,但是保证程序的最终结果和代码顺序执行的结果一致.
int a = 10; //语句1
int b = 20; //语句2
int c = 20; //语句3
c= a + b; //语句4
CPU可能会对没有依赖关系的语句进行重排,比如 2134,3124 但是不会对有依赖关系的数据进行重排比如 3和4 改为4和3 这样就会对结果造成影响.这种重排对单线程是没有任何影响的,但是如果是多线程就可能会出现问题.
验证CPU是否会进行指令重排:
public class Test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 500000; i++) {
Test.State state = new Test.State();
ThreadA t1 = new ThreadA(state);
ThreadB t2 = new ThreadB(state);
t1.start();
t2.start();
}
}
static class ThreadA extends Thread{
private final Test.State state;
ThreadA(Test.State state){
this.state =state;
}
public void run(){
state.a=1;
state.b=1;
state.c=1;
state.d=1;
}
}
static class ThreadB extends Thread{
private final Test.State state;
ThreadB(Test.State state){
this.state =state;
}
public void run(){
if( state.b== 1 && state.a ==0){
System.out.println("b= " + state.b);
}
if(state.c == 1 &&(state.b==0|| state.a ==0)){
System.out.println("c = " + state.c);
}
if(state.d==1 &&(state.a==0||state.b==0||state.c==0)){
System.out.println("d " + state.d);
}
}
}
static class State{
int a = 0;
int b = 0;
int c = 0;
int d = 0;
}
}
/*
c = 1
说明,CPU进行了重排,让c在b或者a前面进行了赋值.
改变顺序可能导致执行结果不同,因此需要禁止重排序。
*/
使用volatile关键字后 就不会出现刚才的情况
static class State{
volatile int a = 0;
volatile int b = 0;
volatile int c = 0;
volatile int d = 0;
}
由此可见:volatile关键字有两个作用1.保证可见性.2禁止重排序
单例设计模式
设计模式 : 不是技术,是以前的人开发人员,为了解决某些问题实现的写代码的经验.
Java的设计模式有23种,分为3个类别,创建型,行为型,功能型
单例代表单个实例,保证一个类的对象永远只有一个!
饿汉式
优点: 简单 多线程下没有任何问题
缺点:
- 当类加载时 对象就会被直接创建
- 若不被使用 对象就白创建了
public class danliDemo1 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Single1.getInstance());
}
}).start();
}
}
}
class Single1 {
private static Single1 s = new Single1();
public Single1() {
}
public static Single1 getInstance(){
return s;
}
}
懒汉式
优点: 延迟加载 什么时候调用方法 什么时候创建对象
缺点: 多线程时 代码有问题
public class danliDemo2 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Single2.getInstance());
}
}).start();
}
}
}
class Single2 {
private static Single2 s;
public Single2() { }
public static Single2 getInstance(){
if (s == null) {
s = new Single2();
}
return s;
}
}
安全问题
一个线程判断完变量 s=null,还没有执行new对象,被另一个线程抢到CPU资源,同时有2个线程都进行判断变量,对象创建多次
性能问题
第一个线程获取锁,创建对象,返回对象. 第二个线程调用方法的时候,变量s已经有对象了,根本就不需要在进同步,不要在判断空,直接return才是最高效的.
双重的if判断,提高效率 Double Check Lock(DCL)
DCL双检查锁机制单例,效率高,线程安全,多线程操作原子性。
class Single2 {
private static Single2 s;
public Single2() { }
public static Single2 getInstance(){
if (s == null) {
synchronized (Single2DCL.class) {
if (s == null) {
s = new Single2DCL();
}
}
}
return s;
}
}
面试题
DCL单例是否需要使用volatile关键字?
需要,单例的模式, 不使用volatile关键字,可能线程会拿到一个尚未初始化完成的对象(半初始化)