并发编程基础1
基础篇之并发编程基础
1. 进程与线程

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建的时候分配的,堆里面主要存放使用new操作创建的对象实例。
方法区则用来存放JVM加载的类,常量及静态变量等信息,也是线程共享的。
2. 线程的创建与运行
Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类并重写run方法,使用FutureTask方式。
- 2.1 继承Thread类方式的实现
public class ThreadTest{
//继承Thread类并重写run方法
public static class MyThread extends Thread{
@override
public void run(){
System.out.println("I am a child thread");
}
}
public static void main(String[] args){
//创建线程
MyThread thread = new MyThread();
//启动线程
thread.start();
}
}
好处:在run()方法内获取当前线程直接使用this就可以了,无需使用Thread.currentThread()方法;不好的地方就在于Java不支持多继承,任务与代码没有分离,多个线程执行一样的任务时需要多份任务代码,而Runnable则没有这个限制。
- 2.2 实现Runnable接口
public static class RunnabTask implements Runnable{
@override
public void run(){
System.out.println("I am a child thread");
}
}
public static void main(String[] args) throws InterruptedException{
RunnableTask task = new RunnableTask();
new Thread(task).start();
new Thread(task).start();
}
上面的两种方式,任务没有返回值。
- 2.3 FutureTask的方式
//创建任务类,类似Runable
public static class CallerTask implements Callable<String>{
@override
public String call() throws Exception{
return "Hello";
}
}
public static void main(String[] args) throws InterruptedException{
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//启动线程
new Thread(futureTask).start();
try{
//等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
}catch(ExecutionException e){
e.printStackTrace();
}
}
CallerTask类实现了Callable接口的Call()方法。在main函数内首先创建一个FutureTask对象(构造函数为CallerTask的实例),然后使用创建的FutureTask对象作为任务创建了一个线程并启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。
3. 线程的等待与通知
3.1 wait() 函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,知道发送下面几件事情之一才返回:(1) 其它线程调用了该共享对象的notify()或者notifyAll( )方法;(2) 其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
另外需要注意的是,如果调用wait()方法的线程没有实现获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常.
线程如何获取共享变量的监视锁:
- 执行synchronize同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){
//doSomething
}
- 调用该共享变量的方法,并且该方法使用了synchronized修饰
synchronized void add(int a, int b){
//doSomething
}
可以预防虚假唤醒的例子
synchronized (obj){
while(条件不满足){
obj.wait();
}
}
//生产线程
synchronized (queue){
//消费队列满,则等待队列空闲
while(queue.size() == MAX_SIZE){
try{
//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取
//该锁,然后获取队列里面的元素
queue.wait();
}catch(Exception ex){
ex.printStackTrace();
}
}
//空闲则生成元素,并通知消费者线程
queue.add(ele);
queue.notifyAll();
}
//消费者线程
synchronized (queue){
//消费队列为空
while(queue.size() == 0){
try{
//挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取
//该锁,将生产者元素放入队列
queue.wait();
}catch(Exception ex){
ex.printStackTrace();
}
}
//消费元素,并通知唤醒生产者线程
queue.take();
queue.notifyAll();
}
当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其它线程中断了该线程,则该线程会抛出InterruptedException异常并返回。
3.2 wait(long timeout)函数
该方法比Wait()方法多了一个超时参数,在wait()方法内部就是调用了wait(0)。
3.3 wait(long timeout, int nanos) 函数
public final void wait(long timeout,int nanos) throws InterruptedException{
if(timeout < 0){
throw new IllegalArgumentException("timeout value is negative");
}
if(nanos < 0 || nanos > 999999){
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if(nanos > 0){
timeout++;
}
wait(timeout);
}
3.4 notify() 函数
唤醒一个共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
3.5 notifyAll() 函数
唤醒一个共享变量上调用wait系列方法后被挂起的线程。
3.4 等待线程终止的join方法
项目实践中遇到这样的场景,就是需要等待某几件事情完成之后才能继续往下执行。Thread类中有一个join方法,可以实现。
例子
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 threadTwo over!");
}
});
//启动子线程
threadOne.start();
threadTwo.start();
System.out.println("wait all chile thread over!");
//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();
System.out.println("all child thread over!");
}
3.5 让线程睡眠的sleep方法
线程调用sleep方法之后会让出指定时间的执行权。
线程在睡眠时拥有的监视器资源不会被释放。
3.6 让出CPU执行权的yield方法
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
3.7 线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void interrupt() 方法:中断线程,设置线程A的中断标志为true。线程实际并未被中断。
- boolean isInterrupted() 方法:检测当前线程是否被中断。
- boolean interrupted(): 与isInterrupted不同的是,此方法会清除标志
4. 理解线程上下文切换
线程上下文切换的时机有:当前线程的CPU时间片使用完处于就绪状态,当前线程被其他线程中断的时候。
6. 线程死锁
6.1 什么是死锁

死锁产生的四个条件:
- 互斥条件:对获取的资源进行排他性使用。
- 请求并持有条件:持有资源并请求新的资源。
- 不可剥夺条件:获得的资源使用完之前不可以被其他线程抢占。
- 环路等待:发生死锁时,必然存在一个线程—资源环形链。
6.2 如何避免死锁
破坏构造死锁的必要条件:其中只有请求并持有条件和环路等待条件是可以破坏的。
破坏持有并等待条件:设定释放资源的时间,不让其永久持有;
破坏环路等待条件:各线程按照相同的顺序来获取资源。
7 . 守护线程与用户线程
用户线程结束后,JVM进程马上结束,无论此时守护进程是否结束。
//设置守护线程的方式
Thread daemonThread = new Thread(new Runnable(){
public void run(){
}
});
daemonThread.setDaemon(true);
8. ThreadLocal
多线程访问同一个共享变量的时候特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程的安全,一般使用者在访问共享变量时需要进行适当的同步。

同步的措施一般是加锁。ThreadLocal可以使得每个线程对其访问的时候访问的是自己线程的变量。
8.1 ThreadLocal使用实例
本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用print函数打印当前本地变量的值。如果打印后调用了本地变量的remove方法,则会删除本地内存中的该变量。
public class ThreadLocalTest{
//(1) print函数
static void print(String str){
//1.1 打印当前线程本地内存中localVariable变量的值
System.out.println(str + ":" + localVariable.get());
//1.2 清除当前线程本地内存中的localVariable变量
//localVaribable.remove();
}
//(2) 创建ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public static void main(String[] args){
//(3)创建线程one
Thread threadOne = new Thread(new Runnable(){
public void run(){
//3.1 设置线程One中本地变量localVariable的值
localVariable.set("threadOne local variable");
//3.2调用打印函数
print("threadOne");
//3.3打印本地变量值
System.out.println("threadOne remove after" + ":" + localVariable.get());
}
});
//(4)创建线程two
Thread threadTwo = new Thread(new Runnable(){
public void run(){
//4.1 设置线程two中本地变量localVariable的值
localVariable.set("threadTwo local variable");
//3.2调用打印函数
print("threadTwo");
//3.3打印本地变量值
System.out.println("threadTwo remove after" + ":" + localVariable.get());
}
});
//(5) 启动线程
threadOne.start();
threadTwo.start();
}
}
8.2 ThreadLocal的实现原理
- ThreadLoacl相关类的类图结构
ThreadLocal类型的本地变量存放在具体的线程内存空间中。
- ThreadLocal的基本方法的实现逻辑
-
void set(T value)
public void set(T value){ //(1) 获取当前线程 Thread t = Thread.currentThread(); //(2)将当前线程作为key,去查找对应线程的变量,找到则设置 ThreadLoaclMap map = getMap(t); if(map != null) map.set(this,value); else //(3)第一次调用就创建当前线程对应的HashMap createMap(t,value); } -
T get()
public T get(){ //(4)获取当前线程 Thread t = Thread.currentThread(); //(5)获取当前线程的threadLocals变量 ThreadLocalMap map = getMap(t); //(6)如果threadLocals不为null,则返回对应本地变量的值 if(map != null){ ThreadLoaclMap.Entry e = map.getEntry(this); if(e != null){ @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //(7)threadLocals为空则初始化当前线程的threadLocals成员变量 return setInitialValue(); } -
void remove()
public void remove(){ ThreadLocalMap m = getMap(Thread.currentThread()); if(m != null){ m.remove(this); } }
在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。
8.3 ThreadLocal不支持继承性
也就是说,同一个ThreadLocal变量在父进程中被设置值后,在子线程中是获取不到的。
8.4 InheritableThreadLocal类
这个类的产生就使得子线程可以访问到父线程中的值。
InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。
InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。

浙公网安备 33010602011771号