四、Java并发编程:并发编程基础

一、线程简介

1. main线程

操作系统每启动一个应用程序都会为其开启一个进程;而在进程里面又可以创建很多线程;Java中运行main方法时,除了main线程,还会启动其他的线程,下面利用JMX来查看:

public static void main(String[] args) {
	//获取Java线程管理MXBean
	ThreadMXBean bean = ManagementFactory.getThreadMXBean();
	//不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
	ThreadInfo[] arr = bean.dumpAllThreads(false, false);
	//遍历线程信息,仅打印线程ID和线程名称信息
	for(ThreadInfo e : arr){
		System.out.println("["+e.getThreadId()+"]"+e.getThreadName());
	}
}

结果

[5]Attach Listener   //提供jvm进程间通信的能力
[4]Signal Dispatcher   //分发处理发送给JVM信号的线程
[3]Finalizer   //调用对象finalize方法的线程
[2]Reference Handler   //清除Reference的线程
[1]main   //main线程

JMX(Java Management Extensions)是一个为应用程序、设备、系统等植入管理功能的框架

2. 为什么要使用多线程

  1. 充分利用多核的处理器

线程是处理器调度的基本单元,一个时刻只可能有一个线程在处理器执行;在多核处理器的情况下,多线程可以利用多核心的特点,使程序执行更加有效率

  1. 提高响应时间

例如:一个订单的创建,包括插入订单数据,生成订单快照,发送邮件通知卖家,记录商品销售数量;单线程情况下,用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果;这样需要客户等待较长的时间;利用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验

3. 线程优先级

处理器调度线程有两种方式:分时调度模型和抢占式调度模型;JVM采用抢占式调度模型,线程优先级越高,获得CPU时间使用权的几率越高

4. 使用工具查看线程状态

使用jconsole.exe和jstack查看线程状态,以死锁为例:

public class Thread_2 {	
	public static void main(String[] args) {
		final A a = new A();
		final B b = new B();
		new Thread(new Runnable() {
			@Override
			public void run() {
				a.invoke(b);
			}
		}, "线程1").start();;
		new Thread(new Runnable() {
			@Override
			public void run() {
				b.invoke(a);
			}
		}, "线程2").start();;
	}
}
class A{
	//① 线程一调用A的invoke()方法,并对a对象进行加锁
	public synchronized void invoke(B b){
		System.out.println(Thread.currentThread().getName()+"进入A的invlke()方法");
		//② 线程一休眠100毫秒,CPU切换执行线程二
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑤ 线程一继续运行,调用B的print方法,但是b对象在第③步被加锁,没有释放锁,所以线程阻塞等待锁释放
		b.print();
	}
	public synchronized void print(){
		System.out.println("A的print()方法");
	}
}
class B{
	//③ 线程二调用B的invoke()方法,并对b对象进行加锁
	public synchronized void invoke(A a){
		System.out.println(Thread.currentThread().getName()+"进入B的invlke()方法");
		//④ 线程二休眠100毫秒,CPU切换执行线程一
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑥ 线程二继续运行,调用A的print方法,但是a对象在第①步被加锁,没有释放锁,所以线程阻塞等待锁释放
		a.print();
	}
	public synchronized void print(){
		System.out.println("B的print()方法");
	}
}
  • jconsole查看(位于JDK安装路径下bin文件夹中jconsole.exe)

四、Java并发编程:并发编程基础_第1张图片

  • jstack
  1. 获取Java线程ID:打开CMD输入jps

四、Java并发编程:并发编程基础_第2张图片
2. 查看堆栈信息:jstack pid

四、Java并发编程:并发编程基础_第3张图片

可以看到,有一条死锁的提示

四、Java并发编程:并发编程基础_第4张图片

二、线程间通信

线程运行的时候拥有独立的栈空间,但是每个线程都在独立运行时,并不能带来很大的价值,只有多个线程相互协作时,才能发挥巨大的作用

1. volatile和synchronized

多个线程访问一个变量时,可以各自copy一份存储于缓存中以提高程序的执行效率;但是这样做可能造成内存可见性问题:一个线程看到的变量可能不是最新的;volatile修饰的变量,使得线程在访问时,只能从内存中访问,完成写操作后必须刷新回主存;synchronized修饰代码块使得多个线程共同访问时,一次只能由一个线程去访问;

  • synchronized
    对于同步块,synchronized使用monitorenter和monitorexit指令进行同步加锁;对于同步方法依靠方法修饰符上的ACC_SYNCHRONIZED来完成;无论哪种方式,其本质是获取对象的监视器(monitor)来实现的

2. 等待/通知机制

等待/通知机制是指A线程调用了对象O的wait()方法进入等待状态,B线程调用对象O的notify()方法或者notifyAll()方法,线程A收到通知后从wait()方法返回,进而进行后续操作。

  • wait()方法
常用方法
  • void wait(): 使当前线程进入阻塞状态,直到调用该对象的notify()或notifyAll()方法
  • void wait(long timeout):使当前线程进入阻塞状态,如果调用该对象的notify()或notifyAll()方法来唤醒线程(进入“就绪状态”),经过指定时间后,线程自动唤醒
  • void wait(long timeout, int nanos):nanos-纳秒;意义同上,对经过时间的控制更为精确
示例

让当前线程阻塞1秒

package com.lt.thread03;

/**
 * wait()方法:
 * @author lt
 * @date 2019年5月9日
 * @version v1.0
 */
public class Thread_01 {

	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("start...");
				try {
					this.wait(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("end...");
			}
		}).start();;
	}
}

结果:

start...
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at com.lt.thread03.Thread_01$1.run(Thread_01.java:17)
	at java.lang.Thread.run(Thread.java:748)

分析:查看IllegalMonitorStateException的JDK源码如下:

Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.

大概意思是:线程尝试获取对象的监视器,或者唤醒其他线程获取对象的监视器;Java中获取监视器只能通过synchronized(lock不可)获取

package com.lt.thread03;

/**
 * wait()方法:
 * @author lt
 * @date 2019年5月9日
 * @version v1.0
 */
public class Thread_02 {

	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("start...");
				try {
					synchronized (this) {
						this.wait(1000);
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("end...");
			}
		}).start();;
	}
}

所以wait()只能在同步的情况下使用

  • notify(),notifyAll()方法

notify()用来唤醒对象monitor上的一个线程,notifyAll()方法用来唤醒对象monitor上的所有线程

示例
package com.lt.thread03;

/**
 * notify()方法:唤醒阻塞的线程
 * @author lt
 * @date 2019年5月9日
 * @version v1.0
 */
public class Thread_03 {

	public synchronized void print(){
		System.out.println(Thread.currentThread().getName()+":start...");
		try {
			this.wait(0);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+":end...");
		
	}
	
	public static void main(String[] args) throws Exception {
		Thread_03 o = new Thread_03();
		for(int i=0; i<5; i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					o.print();
				}
			}, "线程"+i).start();
		}
		synchronized (o) {
			o.notify();
		}
		//为何要sleep(3000)?
		Thread.sleep(3000);
		synchronized (o) {
			o.notifyAll();
		}
	}
}
线程0:start...
线程3:start...
线程4:start...
线程1:start...
线程2:start...
线程4:end...
线程0:end...
线程3:end...
线程1:end...
线程2:end...
思考
  1. 为什么要sleep(3000),否则代码无法继续执行?
  2. Thread.sleep()睡眠的是当前线程?
注意
  1. 使用wait(),notify(),notifyAll()方法之前需要先对调用对象加锁
  2. 调用wait()方法后,线程会释放锁(放弃对象的monitor控制权)
  3. 一个被wait()阻塞的线程,必须同时满足以下两个条件才能被真正执行(线程转换为运行状态)
  1. 线程需要被唤醒(超时唤醒或调用notify/notifyll)
  2. 线程唤醒后需要竞争到锁(monitor)### 1. join()
    A线程中调用B线程的join()方法,A线程会被阻塞进入阻塞状态(Blocked),直到B线程执行完成
package com.lt.thread;

/**
 * 线程常用的方法:join()
 * join():A线程中调用B线程的join()方法,A线程会被阻塞,直到B线程执行完成
 * @author lt
 * @date 2019年4月9日
 * @version v1.0
 */
public class Thread_04 {
	public static void main(String[] args) throws Exception {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i=1; i<=10; i++){
					System.out.println(Thread.currentThread().getName()+"-"+i);
				}
			}
		});
		for(int i=1; i<=10; i++){
			System.out.println(Thread.currentThread().getName()+"-"+i);
			if(i==7){
				t1.start();
				t1.join();
			}
		}
	}
}

结果

main-1
main-2
main-3
main-4
main-5
main-6
main-7
Thread-0-1
Thread-0-2
Thread-0-3
Thread-0-4
Thread-0-5
Thread-0-6
Thread-0-7
Thread-0-8
Thread-0-9
Thread-0-10
main-8
main-9
main-10

3. 管道输入/输出流

管道输入/输出流利用内存作为媒介,来进行线程间的通信;管道输入/输出流包含4种实现:PipedOutputStream,PipedInputStream,PipedWriter,PipedReader

package com.lt.thread07;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.nio.CharBuffer;
import java.util.Scanner;

/**
 * 使用管道输入/输出流进行线程之间的通信
 * @author lt
 * @date 2019年5月18日
 * @version v1.0
 */
@SuppressWarnings("all")
public class PipedTest_1 {

	public static void main(String[] args) throws IOException {
		PipedReader reader = new PipedReader();
		PipedWriter writer = new PipedWriter();
		reader.connect(writer);
		Thread t1 = new Thread(new Runnable(){
			@Override
			public void run() {
				System.out.print("请输入信息:");
				Scanner scanner = new Scanner(System.in);
				while(scanner.hasNext()){
					try {
						writer.write(scanner.next());
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}, "线程1");
		Thread t2 = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					int len = 0;
					while((len=reader.read())!=-1){
						System.out.print((char)len);
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}, "线程2");
		t1.start();
		t2.start();
	}
}
结果
请输入信息:你好!
你好!

线程1接收输入信息,在接收到输入信息后,将其写入PipedWriter中,线程2输出线程1接收到的信息,通过PipedReader打印输入的信息,这样就完成了线程之间的通信;在使用Piped之前,需要调用connect方法进行绑定输入和输出流

5. ThreadLocal

ThreadLocal用于给每个线程提供局部变量,使得一个线程可以在生命周期内去操作这个局部变量;比如Connection对象,在service层开启了事务,却需要在dao层提交/回滚事务,常规你可以将Connection对象作为形参向下传递至dao层,但是有了ThreadLocal,你便可以使用ThreadLocal调用set方法来存放Connection对象,并自动绑定到当前线程,方法执行到dao层,便可以使用使用get方法去获取Connection对象,然后去提交/回滚事务

package com.lt.thread08;

/**
 * @author lt
 * @date 2019年5月19日
 * @version v1.0
 */
public class ThreadLocal_1 {

	public static void main(String[] args) throws Exception {
		ThreadLocal name = new ThreadLocal<>();
		ThreadLocal age = new ThreadLocal<>();
		Thread t1 = new Thread(new Runnable(){
			@Override
			public void run() {
				name.set("李华");
				//只能获取当前线程set的值
				System.out.println(name.get());
				//输出为null
				System.out.println(age.get());
			}
		}, "线程1");
		Thread t2 = new Thread(new Runnable(){
			@Override
			public void run() {
				age.set(23);
				//输出为null
				System.out.println(name.get());
				//只能获取当前线程set的值
				System.out.println(age.get());
			}
		}, "线程2");
		t1.start();
		t2.start();
	}
}

6. sleep()

让当前线程进入阻塞状态(Blocked),暂停指定的时长;睡眠期结束后,线程进入就绪状态(Runnable),并不会立即执行,如果需要立即执行,可以调用sleep(1)

package com.lt.thread;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * sleep()
 * 让当前线程进入阻塞状态,暂停指定的时长;睡眠期结束后,线程进入就绪状态(Runnable),并不会立即执行,
 * 如果需要立即执行,可以调用sleep(1)
 * @author lt
 * @date 2019年4月9日
 * @version v1.0
 */
@SuppressWarnings("all")
public class Thread_05 {
	public static void main(String[] args) throws Exception {
		while(true){
            //隔一秒打印一次时间
			Thread.currentThread().sleep(1000);
			System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
		}
	}
}

结果

2019-05-07 15:52:54
2019-05-07 15:52:55
2019-05-07 15:52:56
2019-05-07 15:52:57
2019-05-07 15:52:58
2019-05-07 15:52:59
2019-05-07 15:53:00
2019-05-07 15:53:01
2019-05-07 15:53:02
2019-05-07 15:53:03
2019-05-07 15:53:04
2019-05-07 15:53:05
2019-05-07 15:53:06
2019-05-07 15:53:07
注意

sleep()和wait()都会让线程阻塞,但是wait()让线程阻塞后立马释放monitor对象锁,使得其他线程可以获取该对象的monitor对象锁;而sleep()使线程阻塞后,在线程暂停期间一直持有monitor对象锁,其他线程不能进入

参考

【1】深入理解 ThreadLocal (这些细节不应忽略)
【2】ThreadLocal使用

你可能感兴趣的:(02.Java,多线程)