java并发编程之美读书笔记前两章




1--80面



java并发编程基础

什么是线程

进程:


是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位


线程:


是进程的一次执行路径,一个进程至少有一个线程,进程中的多个线程共享进程的资源.

线程是CPU分配的基本单位


栈:


每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程无法访问,除此之外栈还可以用来存放线程的调用栈帧.


堆:


堆是一个进程中最大的一个内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例.


方法区:


存放JVM加载的类,常量及静态变量等信息,也是线程共享的.


线程的创建与运行



java一共三种创建线程的方式

  1. 实现Runnable接口的run方法
  2. 继承Thread类并重写run方法
  3. 使用FutureTask方式


/**
* @author xingchen
* @version V1.0
* @Package com.并发.线程的创建
* @date 2023/4/2 15:53
*
*
*
*
*
*
*
*
* 1.使用继承的好处是方便传参,可以在子类里面添加成员方法
* 通过set或者构造函数进行参数传递
* 2.使用Runnable则只能使用主线程里面被声明的final变量.不好的地方是java不支持多继承
* 3.继承Thread那么子类不能再继承其他类 而Runnable没有这个限制
* 4.前两个方法都不支持返回参数,最后的Futuretask方式可以
*
*/
public class ThreadTest {


/**
* 第一种实现多线程的方式
*
* 优点:
* 在run()方法内获取当前线程不需要使用Thread.currentThread()方法,直接使用this就可以获取当前线程
* 缺点:
* java不支持多继承,如果继承Thread就不能继承其他父类
* 任务和代码没分离,当多个线程执行一样的任务时候需要多份认任务代码
* 任务方法没有返回值
*/
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("I am a Thread");
}
}


/**
* 第二种创建线程的方法
*
* 优点:
* 任务跟代码分离开
* 缺点:
* 任务方法没有返回值
*/
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("I am a Thread2");
}
}



public static class CallTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello Thread";
}
}



public static void main(String[] args) throws InterruptedException{
/**
*创建线程的时候其实还没有成功启动,只有执行了线程的start方法的时候才算真正启动成功
* 用操作系统的角度来看创建线程的时候其实处在了就绪态(已经获取了除了CPU之外的其他资源)
* 执行start会执行线程的run方法,就获得了cpu从就绪态转到了运行态
* run方法结束标志该线程运行完成处于终止状态
*
*/
MyThread myThread = new MyThread();

//线程启动
myThread.start();;

RunableTask runableTask = new RunableTask();
new Thread((runableTask)).start();;
new Thread((runableTask)).start();


FutureTask<String> futureTask = new FutureTask<>(new CallTask());
new Thread(futureTask).start();

try {
String string = futureTask.get();
System.out.println(string);
} catch (ExecutionException e) {
e.printStackTrace();
}

}
}


线程的通知和等待

wait()函数


当一个线程调用共享线程的wait()方法的时候,该线程就会被组赛挂起,知道发生以下几种才会返回

  1. 其他线程调用了该共享线程的notify()或者notifyAll()
  2. 其他线程调用了该线程的interrupt(),该线程抛出了中断异常返回


注意:调用wait()方法的时候如果没有提前获取该对象的监视器锁,则调用该方法的时候会抛出IllegalMonitorException异常





获取共享变量的监视器锁的方法

执行synchronized同步代码块时候,使用该共享变量作为参数


synchronized (共享变量){
//代码逻辑
}


调用该共享变量方法的时候,并且该方法使用了synchronized来进行修饰


synchronized void add(int a,int b){
//代码逻辑
}



虚假唤醒


某个线程没有被其他线程调用notify(),notifyAll()方法进行通知,并且没有被中断,等待超时,就从挂起态变成了运行状态

要避免出现这种虚假唤醒

方法: 不断的测试该线程被唤醒的条件是否满足,不满足则继续等待,满足唤醒条件了就退出循环

synchronized (obj){
while (条件不满足){
obj.wait();
}
}



注意:当前线程调用共享变量的wait()方法后只会释放当前共享线程变量上的锁,如果当前线程还持有其他线程的锁是不会被释放的.

当一个线程调用共享对象的wait()方法被阻塞挂起的时候,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回


/**
* @author xingchen
* @version V1.0
* @Package com.并发
* @date 2023/4/2 20:16
*/
public class WaitNotifyInterupt {
static Object object=new Object();
public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("--------begin--------");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

threadA.start();
Thread.sleep(1000);
System.out.println("begin interrput threadA");
threadA.interrupt();
System.out.println("end interrupt threadA---");
}

}


wait(long timeout)函数


相比之前的外滩()方法多了一个超时参数

不同点:

如果一个线程调用共享对象的该方法挂起时,没有在指定的超时时间内被其他线程调用该共享线程的notify()和notifyAll()方法唤醒,就会因为超时而返回

如果调用一个负数则会抛出IllegalArgumentException异常



wait(long timeout,int nanos)函数


public final void wait(long timeout,int nanos) throws InterruptedException{
if(nanos<0){
throw new IllegalAccessException("timeout value is negative");
}
if(nanos<0||nanos>999999){
throw new IllegalAccessException("nanoscond timeout value out of range");
}
if(nanos>0){
timeout++;
}
wait(timeout);
}


notify()函数


会唤醒一个在共享变量上调用的wait()系列方法后被挂起的线程.一个共享变量上可能会有很多个被阻塞的线程在等待,具体唤醒那个线程是随机的

被唤醒的线程不是立马就可以从wait()方法返回并运行,该线程必须在获取了对共享对象的监视器锁后才可以返回

没有获取到监视器锁的话则会抛出IllegalMonitorStateException异常


notifyAll()函数


该方法相比于上一个方法来说,该方法会释放该共享变量上面所有的由于调用wait()系列方法而被挂起的线程

public class NotifyAllTest {
private static volatile Object resourceA=new Object();

public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("threadA get resocuseA lock");
try {
resourceA.wait();
System.out.println("threadA end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}


}
}
});
Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("threadB get resocuseA lock");
try {
resourceA.wait();
System.out.println("threadB end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}


}
}
});
Thread threadC=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("thread begin notify");
resourceA.notifyAll();

}
}
});

threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
threadA.join();
threadB.join();
threadC.join();

System.out.println("over");
}
}

注意:在共享变量上调用notifyAll()只会唤醒该方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。而不会唤醒在该方法之后调用wait系列函数。

等待线程6373等待


等待线程执行终止的join方法


在需要等待多个事件完成后才螚继续往下执行的时候就可以使用Thread方法提供的join()方法

该方法是无参无返回值的方法。


/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/12 23:31
*/
public class JoinTest {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadOne over");
}
});


Thread threadTwo =new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threanTwo over");
}
});

threadOne.start();
threadTwo.start();

System.out.println("wait all child thread over ");

threadOne.join();
threadTwo.join();

System.out.println("all child over");
}
}

线程A调用线程B的join方法后会被阻塞,当其他线程调用线程A的interrupt()方法中断线程A,则A会抛出一个InterruptedException异常

public class InterruptedExceptionTest {
public static void main(String[] args) throws InterruptedException{
Thread threadOne =new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadOne begin run!!!");
for(; ; ){

}
}
});


final Thread mainThread=Thread.currentThread();



Thread threadTwo =new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

mainThread.interrupt();
}
});



threadOne.start();

threadTwo.start();


try {
threadOne.join();
} catch (InterruptedException e) {
System.out.println("main thread "+e);
}
}
}


让线程睡眠都sleep方法


sleep是Thread的一个静态方法,调用该方法会暂时让出指定时间的执行权,也就是不参与CPU调度,但是该线程所拥有的监视器资源(锁)还是持有不让出的,指定的休眠时间到了就会正常返回,相当于从阻塞态变成了就绪态,参与CPU的调度,如果获取到CPU资源之后就可以进入运行态。但是如果在睡眠期间其他线程调用了该线程的Interrupt()方法中断了该线程,那么该方法调用sleep的时候会抛出InterruptedException异常而返回。

/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/12 23:38
*/
public class InterruptedExceptionTest {
//创建一个独占锁
private static final Lock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println("child threadA is in sleep");
Thread.sleep(1000);
System.out.println("child threadA is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});


Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println("child threadB is in sleep");
Thread.sleep(1000);
System.out.println("child threadB is in awake");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});


threadOne.start();

threadTwo.start();

}
}



让出cpu执行权的yield方法


yield是Thread的一个静态方法,当线程调用yield方法时,就是按时线程调度器当前线程请求让出自己的cpu使用 但是线程调度器可以忽略这个暗示。

使用的时候意味着该线程自己占用都时间片没有使用完的部分不想使用了,暗示线程调度器现在就可以进行下一轮的线程调度。当前线程交出cpu使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,也有可能会调度到刚刚让出cpu的那个线程来获取cpu执行权。


总结:

sleep与yield方法的区别

当线程调用sleep方法时会调用线程会被阻塞挂起指定时间,在这期间线程调度器不会去调度该程。

当调用yield时候,线程会让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器在下一次调度的时候就有可能调度刀当前线程执行。


线程中断


线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  1. void Interrupt() :中断线程 例如当线程a运行时,线程b可以调用线程a的interrupt()方法设置a的中断标志为true并返回。设置标志仅仅是设置标志,线程a并没有被中断,他会继续往下执行,如果线程a调用了wait系列函数,join方法或者sleep等方法被阻塞挂起,这时候若是线程b调用a的interrupt()方法,线程a会在调用这些方法的时候抛出InterruptedException异常
  2. boolean isInterrupted() :检测当前线程是否被中断。如果是返回true,否则返回false
public boolean isInterrupted(){
//传递false,说明不清除中断标志
return isIntrrrupted( false);

}
  1. boolean interrupted() :检测当前线程是否被中断,如果是返回true。不同点该方法发现当前线程被中断,会清楚中断标志,并且该方法是static方法,可以通过Thread直接调用在interrupted()内部是获取当前线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
public static boolean Interrupted{
//清除中断标志
return currentThread.isInterrupted(true);
}


一段Interrupt优雅退出的例子


@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()&&more work to do){
//do more work
}
}catch (InterruptedException e){
//thread was interrupted during sleep or wait
}finally {
//cleanup if required
}
}


根据中断标志判断线程是否终止的例子


public static void main(String[] args) throws InterruptedException{
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread()+"hello");
}
}
});
thread.start();
Thread.sleep(1000);
System.out.println("main thread interrupt thread");
thread.interrupt();
thread.join();
System.out.println("main is over");
}


当线程为了等待一些特定条件到来,一般会调用sleep函数,wait系列函数或者join函数来阻塞挂起当前线程,如果提前满足由阻塞到激活态的条件,这时候可以调用该线程的interrupt方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态


public class InterruptedOrIsInterrupted {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
for(; ;){

}
}
});


threadOne.start();

threadOne.interrupt();

System.out.println("isInterrupted:"+threadOne.isInterrupted());

// System.out.println("isInterrupted"+threadOne.interrupted());

System.out.println("isInterrupted:"+Thread.interrupted());

System.out.println("isInterrupted:"+threadOne.isInterrupted());

threadOne.join();
System.out.println("main thread is over");
}
}
/**
* @author xingchen
* @version V1.0
* @Package com.并发
* @date 2023/4/13 9:11
*/
public class InterruptedOrIsInterruptedTwo {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {

}
System.out.println("threadTwo isInterrupted:" + Thread.currentThread().isInterrupted());
}
});



threadOne.start();

threadOne.interrupt();


threadOne.join();
System.out.println("main thread is over");
}
}


理解线程上下文切换


多线程里面,线程数一般是大于CPU个数的,而每个CPU同一时刻只能被一个线程所使用,为了让用户感觉到多个线程在同时使用的,CPU的分配采用了时间片轮转的策略.每一个线程分配一个时间片,线程在该分配的时间片内占用CPU执行任务,当线程片用完之后,就会处于就绪状态并让出CPU让其他线程占用使用.

上下文切换的时候需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场


切换时机:

  1. 当前线程的CPU时间片使用完处于就绪状态
  2. 当前线程被其他线程中断的时候


线程死锁

什么是线程死锁


死锁是指两个或者两个以上的线程在执行过程中,因抢夺资源而造成的互相等待的现象,在无外力的作用下,这些线程会一直等待下去而无法运行


产生死锁的四个必要条件


  1. 互斥条件

线程对已经获取到的资源进行排他性使用,资源同时只能由一个线程占用,如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占用的资源被释放

  1. 请求持有条件

指一个线程已经占有至少一个资源,但又提出新的资源请求,而新的资源已经被其他线程占用,所以当先线程被阻塞,但阻塞的同时并并不释放自己已经获得的资源

  1. 不可剥夺条件

指线程获取的资源在自己使用完之前别的线程不能抢占,只有在自己使用完之后释放了才能被其他线程使用

  1. 环路等待条件

指发生死锁的时候必然存在一个线程一个资源的环形链

/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/13 9:35
*/
public class DeadLockTest2 {
private static Object resourceA=new Object();
private static Object resourceB=new Object();

public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println((Thread.currentThread() + "get ResourceA"));
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceB");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceB");
}
};
}
});


Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB){
System.out.println((Thread.currentThread() + "get ResourceB"));
try{
Thread.sleep(1000);

}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceA");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceA");

}
};
}
});
}
}


避免线程死锁


只需要破坏至少一个死锁的必要条件即可,但是目前其实能被破坏的只有请求并持有环路等待条件是可以被破坏的

造成死锁其实也和申请资源的顺序有很大的关系,使用资源的有序性原则就可以破坏避免死锁

举例:对上述代码的线程B进行修改

Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println((Thread.currentThread() + "get ResourceB"));
try{
Thread.sleep(1000);

}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceA");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceA");

}
};
}
});


守护线程与用户线程


  1. daemon线程(守护线程)
  2. user线程(用户线程)

main函数就是属于用户线程JVM启动的时候会调用main线程,但其实还调用了许多其他的守护线程


两者的区别


当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否由守护线程,也就是说守护线程不影响JVM的退出.也就是说只要有一个用户线程没有结束,正常情况下JVM就不会退出

/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/13 16:44
*/
public class DaemonTest {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {

}
});

/**
* 只需要设置参数为true就开启了守护线程
*
*/
thread.setDaemon(true);
thread.start();
}
}


举例理解守护线程和用户线程的区别


public class DaemonOrUserTest {

/**
*thread线程里面是一个无限循环,运行之后主线程已经结束,但是jvm还没有退出
* 说明父线程结束后,子线程还可以继续存在,也就是子线程的生命周期并不受父线程的影响
* 也说明了在用户线程还存在的情况下JVM进程并不会终止
*
*
*
*
* 设置成用户线程之后一旦主线程停止之后,jvm发现已经不存在用户线程了,就会终止JVN进程
* 如果用户进程已经结束,但是守护线程还说运行,这个时候JVM不需要等待守护线程停止就直接结束JVM进程
*
*
*
*
* main线程运行结束之后,JVM会自动启动一个DestroyJavaVm 的线程,该线程等待所有的用户线程结束后
* 终止JVM进程
*
*
*/
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
for (; ;){

}
}
});

thread.setDaemon(true);
thread.start();
System.out.println("main thread is over");
}



}


总结


如果希望在主线程结束后JVM立马结束,那么在创建进程的时候可以将其设置为守护线程,否则的话就设置为用户线程


ThreadLocal


多线程访问退役个共享变量的时候容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用访问者在访问共享变量时候需要进行适当的同步


同步一般情况下使用加锁,但是这种方式加重了使用者的负担.

那么可以使用创建一个变量后,每一个线程对其访问的时候访问的是自己线程的变量,本节的ThreadLocal就是这个作用.

ThreadLocal

是由JDK提供的,提供了线程本地变量,如果创建了一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的本地副本

当多个线程操作这个变量的时候,实际操作的是自己本地内存的变量,从而避免了线程安全问题


使用实例


public class ThreadLocalTest {
static void print(String str){
System.out.println(str+":"+localVariable.get());
//localVariable.remove();

}
static ThreadLocal<String> localVariable=new ThreadLocal<>();
public static void main(String[] agrs){
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after"+":"+localVariable.get());
}
});
Thread threadTwo=new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("threadTwo local variable");
print("threadOne");
System.out.println("threadTwo remove after"+":"+localVariable.get());
}
});



threadOne.start();
threadTwo.start();

}
}


实现原理


set方法

public void set(T value){
Thread t=Thread.currentThread();
ThreadLoalMap map=getMap(t);
if(map!=null){
map.set(this,value);
}else {
createMap(t,value);
}
}
ThreadLocalMap getMap(Thread t){
return t.threadLocals;
}

getMap(t)

  1. 方法的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到线程的成员变量上面
  2. 如果个体Map(T)返回值不为空,则把value设置到threadLocals
  3. 如果返回值为空则说明第一次调用的set方法,这时候创建当前线程的threadLocals变量

createMap(t,value)

创建当前线程的threadLocals变量

void createMap(Thread t,T firstValue){
t.threadLocals=new ThreadLocalMap(this,firstValue);
}




T get()方法


public void get(){

//获取当前线程
Thread t=Thread.currentThread();
//获取当前线程的threadLocxals变量
ThreadLocalMap map=getMap(t);
//如果ThreadLocals不为空,则返回对应的本地变量
if(map!=null){
ThreadLocalMap.Entry e=map.getEntry(this);
}
if(e!=null){
@SuppressWarnings("unchecked")
T result=(T) e.value;
return result;
}
//为空初始化当前线程的ThreadLocals成员变量
return setInitialValue;
}

private T setInitialValue(){
//初始化为null
T value =initialValue;
Thread t=Thread.currentThread();
ThreadLocalMap map=getMap(t);
//如果当前线程的本地变量不为空
if(map!=null){
map.set(this,value);

}else {
//如果当前线程的本地变量为空 则创建
createMap(t,value);

}
return value;
}
private T initialValue(){
return null;
}


void remove()


/**
* 如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量
*/
public void remove(){
ThreadLocalMap map=getMap(Thread.currentThread());
if(m!=null){
m.remove(this);
}
}


不支持继承性


public class TestThreadLocal1 {
public static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args){
threadLocal.set("hello world");
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread:"+threadLocal.get());
}
});
thread.start();
System.out.println("main:"+threadLocal.get());
}
}

同一个ThreadLocal变量在父线程中被设置后,在子线程是获取不到的

在子线程里面调用的get方法是当前线程,而调用的set方法是设置的main线程

两者是不同的线程


InheritableThreadLocal类


继承自ThreadLocal,提供了一个特性,让子线程可以访问在父线程中设置的本地变量

public class InheritableThreadLocal<T> extends ThreadLocal{
protected T childValue(T parentValue){
return parentValue;
}
ThreadLocalMap.getMap(Thread t){
return t.inheritableThreadLocals;
}
void create(Thread t,T firstValue){
t.inheritableLocals=new TestThreadLocalMap(this,firstValue);
}
}









总结:

InheritableThreadLocal类重写代码让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get()方法设置变量时,就会创建当前线程的InheritableThreadLocals变量,当父线程创建子线程时,构造函数就会把父线程的InheritableThreadLocals变量里面的本地变量复制一份保存到子线程的InheritableThreadLocals变量里面.



并发编程的其他基础知识

什么是多线程的并发编程


并发:

同一时间段内多个任务同时都在执行,且执行都没有执行结束,强调的是在一个时间段内同时执行,而一个时间段由多个时间积累而成的,所以并发的多个任务在单位时间内并不一定同时执行

并行:

单位时间内多个任务同时在执行


为什么要进行多线程并发编程


多核CPU时代打破了单核CPU对多线程的性能限制,多个CPU"意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但是随着对应用系统性能和吞吐量要求的提高,出现了海量数据和请求的要求,迫切需要高并发编程


java的线程安全问题


共享资源:

该资源被多个线程所持有或者说多个线程都可以访问该资源


线程安全问题:

当多个线程同时读写一个共享资源并且没有任何同步措施的时候,导致出现脏数据或者不可预见结果的其他问题


java中共享变量的内存可见性问题


将所有的变量都存放在主内存,当线程使用变量的时候,会把主内存的变量复制到自己的工作空间或者工作内存,线程读写变量操作的是自己工作内存中的变量


java中的synchronized关键字

关键字介绍


java提供的一个原子性内置锁,java的每一个对象都可以把他当作同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,与叫做监视器锁.

也是一种排他锁,也就是一个线程获取了这个锁后,其他线程必须等待该线程释放锁后才能获取该锁

java中的线程和操作系统的原生线程一一对应,当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的,而synchronized的使用就会导致上下文切换


内存语义


把在synchronized块内使用到的变量从线程的工作内存中清楚,这样子在synchronized块内使用的变量就不会从线程的工作内存中获取,而是从主内存中获取,退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存

而synchronized的使用就会导致上下文切换带来线程调度开销


java中的volatile关键字


确保对一个变量的更新对其他线程马上可见,当一个变量使用volatile关键字时,线程在写入变量的时候就不会把值缓存在寄存器或者其他地方,而是会把值刷新到主内存,当其他线程读取该共享变量的时候,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值


内存语义


和synchronized相似,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存)读取volatile变量值时就相当于进入了同步块(先清空本地内存变量值,再从主内存获取最新值).

public class ThreadNotSafeIntegerTest {
private int value;
public int get(){
return value;
}
public void set(int value){
this.value=value;
}
}


synchronized 方法

public class ThreadNotSafeIntegerTest {
private int value;
public synchronized int get(){
return value;
}

public synchronized void set(int value){
this.value=value;
}
}

volatile方法

public class ThreadNotSafeIntegerTest {
private volatile int value;
public int get(){
return value;
}

public void set(int value){
this.value=value;
}
}

volatile并不能保证操作的原子性


什么时候使用volatile关键字


写入变量值不依赖变量的当前值

因为如果依赖当前值,将获取-计算-写入三步操作,这三步不是原子性操作,而volatile不能保证原子性

读写变量值没有加锁

因为加锁本身已经保证了内存可见性,这时候不需要啊变量声明为volatile


java中的原子操作


一系列操作时,这些操作要么全部执行,要么全部不执行,不存在执行性其中一部分的情况.

如下代码就是线程不安全的问题

public class ThreadNotSafeCount {
private Long value;
public Long get(){
return value;
}
public void inc(){
++value;
}
}

使用synchronized修饰就可以保证原子性

public class ThreadNotSafeCount {
private Long value;

public synchronized Long get(){
return value;
}
public synchronized void inc(){
++value;
}
}


java中的CAS操作


锁在并发处理中占据一席之地,但是当一个线程没有获取到锁时就会被阻塞挂起,导致线程上下文的切换喝调度开销,java提供了非阻塞的volatile关键字来解决共享变量的可见性,一定程度上解决了锁带来的开销问题,但是只能保证共享变量的可见性,不能解决读-改-写等原子性操作

CAS即 Compare and Swap 是JDK提供的非阻塞原子性操作,通过硬件保证了比较--更新操作的原子性

JDK里面Unsafe提供了一系列的compareAndSwap方法

boolean compareAndSwapLong(Object obj,long valueOffset,long expect,long update)方法

比较并交换

四个参数分别是

  1. 对象内存位置
  2. 对象中变量的偏移量
  3. 变量预期值
  4. 新的值
  5. 如果对熊obj中内存偏移量为valueOffset的变量的值为expect,则使用新的值update置换旧的值expect


ABA问题


关于CAS 操作有个经典的ABA 问题,具体如下:假如线程I使用 CAS 修改初始值为A的变量X,那么线程I会首先去获取当前变量X 的值(为A),然后使用 CAS 操作尝试修改X的值为 B,如果使用CAS 操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I获取变量X的值A后,在执行 CAS 前,线程I使用CAS修改了变量X的值为 B,然后又使用 CAS 修改了变量X 的值为 A。所以虽然线程I执行 CAS时X的值是A,但是这个A已经不是线程I获取时的A了。这就是 ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B,B到C,不构成环形,就不会存在问题。JDK 中的AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。


Unsafe

Unsafe类的重要方法



如何使用Unsafe类


public class UnsafeTest {

//获取实例
static final Unsafe unsafe=Unsafe.getUnsafe();
//记录state变量在类中的偏移值

static final long stateOffset;

private volatile long state=0;

static {
try {
stateOffset=unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("statee"));

}catch (Exception e){
System.out.println(e.getLocalizedMessage());
throw new Error(e);
}
}

public static void main(String[] args) {
UnsafeTest unsafeTest=new UnsafeTest();
Boolean success=unsafe.compareAndSwapInt(unsafeTest,stateOffset,0,1);
System.out.println(success);
}

}


运行结果


@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}



public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}

正规渠道不能使用Unsafe方法,要想使用可以用反射方法来获取Unsafe的实例方法

public class TestUnsafe {
static final Unsafe unsafe;

static final long stateOffset;

private volatile long state=0;
static {
try {
Field file=Unsafe.class.getDeclaredField("theUnsafe");
file.setAccessible(true);
unsafe=(Unsafe) file.get(null);
stateOffset=unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
}catch (Exception e){
System.out.println(e.getLocalizedMessage());
throw new Error(e);
}
}

public static void main(String[] args) {
TestUnsafe test=new TestUnsafe();
Boolean success=unsafe.compareAndSwapInt(test,stateOffset,0,1);
System.out.println(success);
}
}


java指定重排序


java内存模型允许编译器喝处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序,在单线程下重排序可以保证最终的执行结果与程序顺序执行结果一致,但是在多线程下存在安全问题

public class MoreThreadTest {


/**
* 这段代码没有声明volatile变量,也没有使用任何同步措施
* 多线程下存在共享内存可见性问题
* 可以通过对共享变量声明成volatile就可以避免指令重排序问题
*/
public static class ReadThread extends Thread{
@Override
public void run(){
while (!Thread.currentThread().isInterrupted()){
if(ready){
System.out.println(num+num);
}
System.out.println("read thread");
}
}

}
public static class WriteThread extends Thread{
@Override
public void run(){
num=2;
ready=true;
System.out.println("writeThread set over");
}
}
private static int num =0;
private static boolean ready=false;

public static void main(String[] args) throws InterruptedException{
ReadThread readThread=new ReadThread();
readThread.start();
WriteThread writeThread=new WriteThread();
writeThread.start();
Thread.sleep(1000);
readThread.interrupt();
System.out.println("main exit");

}

}

当volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后,读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前


伪共享

伪共享是什么


为了解决计算机系统中内存与CPU之间运行速度差问题,会在CPU与主内存之间添加一级或者多级高速缓存存储器(Cache)

一般被集成在CPU内部(CPU Cache)


在Cache内部是按行存储的,其中一行称为一个Cache行,Cache行是Cache与主内存进行数据交换的单位,每行的大小一般为2的幂次方字节


当CPU访问某个变量的时候,首先回去看CPU Cache内是否存在变量,如果有则直接从其中获取否则就去主内存中获取变量,然后把该变量所在的内存区域的一个Cache行大小的内存复制到Cache,由于存放到Cache行的内存块而不是单个变量,所以可能把多个变量存放到同一个Cache中,当多个线程同时修改一个缓存行的里面的多个变量的时候,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享


多个线程不能同时去修改自己所使用的CPU中相同缓存行里面的变量,更坏的情况是,如果CPU只有一级缓存,则会导致频繁的访问主内存



为什么会出现伪共享


是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同的变量

原因:

因为缓存与内存交换的数据的单位就是缓存行,所以多个变量会被放入同一个缓存行

public class ForContentTest {
static final int LINE_NUM=1024;
static final int COLUM_NUM=1024;

public static void main(String[] args) {
long[][] array=new long[LINE_NUM][COLUM_NUM];
long startTime =System.currentTimeMillis();

for (int i=0;i<LINE_NUM;++i){
for (int j=0;j<COLUM_NUM;++j){
array[i][j]=i*2+j;

}
long endTime=System.currentTimeMillis();
long cacheTime=endTime-startTime;
System.out.println("cache time :"+cacheTime);
}
}

}

public class ForContentTest2 {
static final int LINE_NUM=1024;
static final int COLUM_NUM=1024;

public static void main(String[] args) {
long[][] array=new long[LINE_NUM][COLUM_NUM];
long startTime =System.currentTimeMillis();

for (int i=0;i<LINE_NUM;++i){
for (int j=0;j<COLUM_NUM;++j){
array[j][i]=i*2+j;

}
long endTime=System.currentTimeMillis();
System.out.println("no cache time :"+(endTime-startTime));
}
}
}

代码一比代码二快了不少原因

数组中数组元素的内存地址是连续的,当访问数组的第一个元素的时,会把第一个元素后的若干个元素一块存入缓存行,这样子顺序访问数组里面的元素时直接命中,就不会去主内存读取了.

第二个则是跳跃式的访问数组元素,不是顺序的,这样子破坏了程序访问的局部性原则,并且缓存是容量控制的,当缓存满了会根据一定淘汰算法替换缓存行,这会导致从内存置换过来的缓存行的元素还没等读取到就被置换了


所以在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序员运行的局部性原则,从而加速程序的运行,而在多线程下修改一个缓存行的多个变量时候就会竞争缓存行,从而降低程序运行性能



如何避免伪共享


通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充改变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中

public final static class FilledLong{
public volatile long value;
public long p1,p2,p3,p4,p5,p6;
}

假设缓存行为64个字节'

这六个变量加上volatile变量以及一个FilledLong类对象一共占用64个字节正好是可以存入同一个缓存行




小结


本节讲述了伪共享如何产生,如何避免,并证明在多线程下访问同一个缓存行的多个变量时才会出现伪共享,在单线程下访问一共缓存行里面的多个变量反而会对程序运行起到加速作用


锁的概述

乐观锁与悲观锁


悲观锁:

对数据被外界修改保持保守态度,认为数据很容易被其他线程修改,所以在数据被处理之前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态

实现:

往往依赖数据库提供的锁机制,即数据库中,在对数据记录操作前给记录加排他锁,如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常,如果获取锁成功,则对记录进行操作,然后提交事务后释放排他锁

乐观锁:

数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在数据提交更新时候,才会正式对数据冲突与否进行检测.


公平锁和非公平锁


根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁

公平锁:

表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程最早获取到锁

非公平锁:

不一定先到先得


ReentrantLock提供了公平锁和非公平锁的实现

  1. 公平锁:ReentrantLock pairLock=new ReentrantLock(true)
  2. 非公平锁:ReentrantLock pairLock=new ReentrantLock(false)

如果不传递参数默认是非公平锁

公平锁会带来性能的开销


独占锁和共享锁


根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁

独占锁:

能保证任何时候都只能有一个线程得到锁ReentrantLock就是以独占锁方式实现的

是一种悲观锁,由于每次访问资源都先加上互斥锁,限制了并发性,因为操作并不会影响数据的一致性,而独占锁只允许同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取

共享锁:

ReadWriteLock读写锁,允许一个资源被多个线程同时进行读操作是一种乐观锁,放宽了加锁的条件,允许多个线程同时进行读操作


什么是重入锁


当一个线程要获取一个被其他线程持有的独占锁时候,该线程会阻塞,

那么当一个线程再次获取他自己已经获取的锁时候是否会被阻塞呢?

如果不被阻塞那么该锁是可重入的,也就是说只要该线程获取了该锁,就可以无限次地进入该锁锁住的代码


自旋锁


当前线程在获取锁的时候,如果发现锁已经被其他线程占用,不会马上阻塞自己,在不放弃CPU的使用权的情况下,多次尝试获取(默认次数10)很有可能后面几次尝试中其他线程已经释放了锁,如果尝试指定次数之后仍然没有获取到锁则当前线程则会被阻塞挂起,自旋锁是适应了CPU时间旱区线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费啦


总结


本章主要讲述并发编程的基础知识,为后面在高级篇讲解并发包源码打下了基础并结合图示形象的讲述了为什么要使用多线程编程,多线程编程存在的线程安全问题,以及什么是内存可见性问题,然后讲述了synchronized和volatile关键字,并且强调前者既保证内存的可见性又保证了原子性,后者则主要保存了内存可见性,但是二者的内存语义非常相似,最后讲解了什么是CAS和线程间同步以及各组锁概念








0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
林寻
下载 APP