多线程
多线程是提升程序性能非常重要的一种方式,使用多线程可以让程序充分利用CPU资源,提高CPU的使用效率,从而解决高并发带来的负载均衡问题。它的优点有:(1)资源得到更合理的利用;(2)程序设计更加简洁;(3)程序响应更快,运行效率更高。缺点是:(1)需要更多的内存空间来支持多线程;(2)多线程并行访问的情况可能影响数据的准确性;(3)数据被多线程共享,可能出现死锁的情况。
什么是进程?什么是线程?
进程是计算机正在运行的一个独立的应用程序,例如打开一个浏览器就是运行一个进程,一个应用程序至少有一个进程。
线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。
进程与线程的区别在于进程在运行时拥有独立的内存空间,即每个进程所占用的内存都是独立的,互不干扰。而多个线程是共享内存空间的,但是每个线程的执行是相互独立的,同时线程必须依赖于进程才能执行,单独的线程是无法执行的,由进程来控制多个线程的执行。
通常意义的多线程是指在一个进程中,多个线程同时执行。注意这里的同时执行不是真正意义上的同时执行,系统会自动为每个线程分配CPU资源,在某个具体时间段内CPU的资源被一个线程占用,在不同的时间段内由不同的线程来占用CPU资源,所以多个线程还是在交替执行,只不过因为CPU运行速度太快,感觉上是在同时执行。
线程的使用
Java中实现多线程的常用方式有两种:基础Thread类和实现Runnable接口。
继承Thread类
继承Thread类的实现步骤:(1)创建自定义类并继承Thread;(2)重写Thread的run()方法,并编写该线程的业务逻辑代码
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 1000; i++){
System.out.println("+++++ Thread +++++");
}
}
}
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 1000; i++){
System.out.println("----- MyThread -----");
}
}
}
从上面程序的运行结果可以看到,当前程序中有两个线程,一个是主线程即main方法,另一个是子线程MyThread。需要注意的是开启子线程是通过调用线程对象的start()方法来完成的,一定不能调用run()方法,调用run()方法是普通的方法调用,相当于在主线程中顺序执行了两个循环,并没有开启一个可以和主线程抢占CPU资源的子线程。所以调用run()方法并不是多线程,还是一个主线程执行。
实现 Runnable 接口
Runnable接口的实现步骤:(1)创建自定义类并实现 Runnable接口;(2)实现 run()方法,编写该线程的业务逻辑代码
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++){
System.out.println("----- myRunnable ------");
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i = 0; i < 100; i++){
System.out.println(" Runnable ");
}
}
}
MyRunnable的使用与MyThread略有不同,MyRunnable相当于定义了线程业务逻辑,它本身并不是线程对象,所以还需要实例化Thread对象,然后将MyRunnable对象赋给Thread对象。然后调用Thread对象的start()方法来启动该线程。
线程的状态
线程共有5种状态,在特定的情况下,线程可以在不同的状态之间切换,5种状态如下:
- 创建状态:实例化了一个新的线程对象,还未启动。
- 就绪状态:创建好的线程对象调用start()方法完成启动,进入线程池等待抢占CPU资源。
- 运行状态:线程对象获取了CPU资源,在一定的时间内执行任务。
- 阻塞状态:正在运行的线程暂停执行任务,释放所占用的CPU资源。并在解除阻塞之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取CPU资源。
- 终止状态:线程运行完毕或因为异常导致该线程终止运行。

线程调度
线程休眠
休眠指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程的一种调度方式,要通过调用sleep()方法来实现。
sleep(long millis)是Java.lang.Thread类中定义的方法,使用时需要指定当前线程休眠的时间,传入一个long类型的数据作为休眠时间,单位为毫秒。任何一个线程的实例化对象都可以调用该方法。
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 100; i++){
if (i == 10){
try{
sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("----- MyThread -----");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
for (int i = 0; i < 100; i++){
System.out.println(" Thread ");
}
}
}
运行上面代码可以发现,当子线程中 i=10 时,会休眠1000毫秒,1000毫秒之后线程进入就绪状态,重新等待系统为其分配CPU资源。这是在线程内部执行休眠操作;也可以在外部使用线程时执行休眠操作,如下所示。
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 100; i++){
System.out.println("----- MyThread -----");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
try{
thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
thread.start();
for (int i = 0; i < 100; i++){
System.out.println(" Thread ");
}
}
}
主程序中创建子线程,程序运行时子线程先休眠5000毫秒之后再启动。其他线程也是,经过5000毫秒后竞争CPU资源。
主线程并不是一个手动实例化的线程对象,不能直接调用sleep()方法,这种情况下可以通过Thread类的静态方法currentThread来获取主线程对应的线程对象,然后调用slepp()方法。
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++){
if (i == 2){
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(" Thread ");
}
}
}
注意:无论通过那种方式调用sleep()方法都需要注意处理异常,因为sleep()方法在定义时声明了可能会抛出的异常InterruptedException。
线程合并
合并的意思是将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中两个子线程顺序执行,即一个线程执行完毕之后再来执行第二个线程,通过线程对象的join()方法来实现合并。
线程甲在执行到某个时间点的时候调用线程乙的join()方法,则表示从当前时间点开始CPU被线程乙独占,线程甲进入阻塞状态。直到线程乙执行完毕,线程甲进入就绪状态,等待获取CPU资源进入运行状态继续执行。
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 100; i++){
System.out.println(i + "----- MyThread -----");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
for (int i = 0; i < 100; i++){
if (i == 5){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i + " MainThread ");
}
}
}
在Main类的主线程中开启子线程。当主线程循环执行到 i==5 的节点时,将子线程合并到主线程中,此时主线程进入阻塞状态。而后子线程执行,当子线程执行完毕之后,主线程继续执行。
join()方法存在重载: join(long millis),如果某个时间点在线程甲中调用了线程乙的sleep(1000)方法,表示从当前这一时刻起,线程乙会独占CPU资源,线程甲进入阻塞状态。当线程乙执行了1000毫秒之后,线程甲重新进入就绪状态。此时,无论线程乙是否允许完成,线程甲都会来竞争CPU资源。而调用join()方法,则是线程乙允许完成后,线程甲才来竞争资源。
线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,即从运行状态或就绪状态来到阻塞状态,从而将CPU资源让给其他线程来使用。Java中的线程礼让,通过调用yield()方法完成。
假如有线程甲和线程乙在交替执行,在某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了CPU资源,执行其业务逻辑,但不是说线程甲会一直暂停执行,直到线程乙执行完毕再来执行线程甲。线程甲只是在特定的时间节点礼让,过了这个时间节点,线程甲再次进入就绪状态,和线程乙再次争夺CPU资源。
线程中断
有多种情况可以造成线程停止运行,例如线程执行完毕之后会自动停止该线程;线程执行过程中遇到错误会抛出异常并停止该线程;线程在执行过程中会根据需求手动停止该线程。
Java中实现线程中断机制有如下几种方法:
public void stop()public void interrupt()public void isInterrupted()
stop()方法在新版本的JDK中已经不推荐使用。
interrupt是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象,每个线程对象都是通过一个标志位来判断当前是否为中断状态。
isInterrupted()方法就是用来获取当前线程对象的标志位,true表示清除了标志位,当前线程对象已经中断;false表示没有清除标志位,当前对象没有中断。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}
// 输出
NEW
false
getState()方法可以获取当前线程对象的状态,实例化一个线程对象thread,但是并未启动该对象,直接中断。
从输出结果可以看到,NEW表示当前线程对象为创建状态。false表示当前线程并未中断,因为当前线程没有启动,所有不存在中断,不需要清除标志位。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 5){
Thread.currentThread().interrupt();
}
System.out.println("runTime .......");
}
}
});
thread.start();
System.out.println(thread.getState());
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
// 输出
??????? p192
线程同步
Java中允许多线程并行访问,即同一个时间段内多个线程同时完成各自的操作。但是这样会带来一个问题,当多个变量同时操作一个共享数据时,可能会导致数据不准确的问题。
public class MyThread extends Thread{
private static int num = 0;
public void run(){
try {
Thread.currentThread().sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(Thread.currentThread().getName() + "是当前的第" + num + "个访问者!");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread, "线程1");
Thread t2 = new Thread(myThread, "线程2");
t1.start();
t2.start();
}
}
// 输出
线程1是当前的第2个访问者!
线程2是当前的第2个访问者!
从输出结果可以看到此时访问数据是有问题的,两个线程都显示为第2个访问者。问题出现的原因是两个线程在同时访问静态资源num。
这个是多线程同时访问共享数据时带来的隐患。解决方法是使用线程同步。可以通过 synchronized 修饰方法来实现线程同步,每个Java对象都有一个内置锁,内置锁会保护使用 synchronized 关键字修饰的方法,要调用该方法必须先获得内置锁,否则就处于阻塞状态。
public class MyThread extends Thread{
private static int num = 0;
public synchronized void run(){ // 添加 synchronized
try {
Thread.currentThread().sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(Thread.currentThread().getName() + "是当前的第" + num + "个访问者!");
}
}
// 输出
线程1是当前的第1个访问者!
线程2是当前的第2个访问者!
代码的运行逻辑是:假设线程1先到,获取了run()方法的锁,之后线程2到了,发现 run()方法被锁起来了。要调用方法必须拿到锁,但是此时锁被线程1获取了,只有当线程1调用完方法之后才会释放锁。执行了一次num++,然后输出信息,之后线程1释放内置锁。线程2获取到了内部锁,由阻塞状态进入运行状态,调用run()方法,再一次执行num++并输出信息。两个线程是按照先后顺序来执行的,并没有同时去修改num,所以看到了正确的结果。
synchronized 可以修饰实例方法,也可以修饰静态方法,但是两者在使用上有区别。
修饰静态方法
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Main.test();
}
});
thread.start();
}
}
private static void test() {
System.out.println("start...");
try{
Thread.currentThread().sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("end...");
}
}
// 输出
start...
start...
start...
end...
end...
end...
从运行结果可以发现,运行结果不是“start…”“end…”成对输出,而是单独输出。原因是多线程并行访问,一个线程输出了“start…”之后,休眠1000ms,在输出“end…”,所有就给了其他线程输出的机会。现在使用synchronized 关键字分别修饰静态方法和实例方法。
修饰静态方法
private synchronized static void test() {
System.out.println("start...");
try{
Thread.currentThread().sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("end...");
}
// 输出
start...
end...
start...
end...
start...
end...
此时程序运行的结果是“start..”“end…”成对输出,因为我们给test()方法加了一把锁,线程1先到,拿到了这把锁,然后执行test的业务逻辑。在整个执行过程中,其他线程到了也只能处于阻塞状态等待,因为它们拿不到锁。只有当线程1执行完毕释放了锁,其他线程才可以拿到锁进而执行方法。所以执行顺序是线程按序执行。
修饰实例方法
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
new Main().test();
}
});
thread.start();
}
}
private synchronized void test() {
System.out.println("start...");
try{
Thread.currentThread().sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("end...");
}
}
// 输出
start...
start...
start...
end...
end...
end...
运行程序结果可以看到,此时的内置锁并没有为一个线程锁定资源,加锁的效果并没有实现。这是因为当前锁定的是一个实例方法,每一个线程都有这样的一个实例方法,相互之间是独立的。即test方法并不是被所有线程所共享的,实际的运行情况是每一个线程都获取自己的锁,然后并行访问,相互之间并没有“你运行、我等待”的关系,所以给实例方法添加synchronized关键字并不能实现线程同步。
在前面统计访问量的代码中,synchronized修饰的也是实例方法,为什么就可以实现同步呢?因为实例方法中操作的变量num是静态的,所以还是多线程在共享资源,线程同步的本质是锁定多个线程所共享的资源。synchronized还可以修饰代码块,会为代码块加上内置锁,从而实现同步。
修饰代码块
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Main.test();
}
});
thread.start();
}
}
private static void test() {
synchronized (Main.class){
System.out.println("start...");
try{
Thread.currentThread().sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("end...");
}
}
}
// 输出
start...
end...
start...
end...
start...
end...
synchronized()内设置需要加锁的资源,静态方法是属于类的方法,不属于任何一个实例对象,所以静态方法中的 synchronized()只能锁定类,不能锁定实例。同理,静态方法中也不能锁定实例变量,只能锁定静态变量。
在实例方法中添加synchronized()修饰的实例方法,结果是没有锁住资源,因为synchronized()锁定的是this,即当前的实例化对象。每个线程都有一个实例对象,相互独立,并不是共享资源,所有没有实现线程同步。改进方法是:需要锁住共享资源,实例对象是每个线程独有的,类则是共享的,所以锁住类即可
线程安全的单例模式
单例模式是一种常见的软件设计模式,其核心思想是一个类只有一个实例对象,由多个线程来共享该实例对象资源。在某些系统中,只有一个实例对象资源很重要。例如售票系统,共1000张票,分10个窗口出售,则这10个窗口就必须共享这1000张票的实例对象。如果每个窗口都有自己的实例对象,就变成了每个窗口都有1000张票可以出售,有悖于真实逻辑。单例模式实现核心是共享实例对象,那么就把实例对象定义为静态。
public class MyThread extends Thread{
private static MyThread instance;
private MyThread(){
System.out.println("creatMyThread...");
}
public static MyThread getInstance(){
if (instance == null){
instance = new MyThread();
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
MyThread instance1 = MyThread.getInstance();
MyThread instance2 = MyThread.getInstance();
}
}
// 输出
creatMyThread...
这里虽然只创建了一个实例对象,也实现了共享,但是并不是真正的单例模式。并且在编写代码时需要考虑多线程并行访问的情况,现在将代码修改为多线程并行访问。
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
MyThread instance = MyThread.getInstance();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
MyThread instance = MyThread.getInstance();
}
}).start();
}
}
// 输出
creatMyThread...
creatMyThread...
这里创建了两个实例对象,原因是线程1和线程2是并行访问的。线程1先来判断 instance==null 是成立的,然后线程1来实例化对象。正在此时,实例化对象的操作还没有完成,线程2来了,先判断 instance==null 是成立的,于是线程2也执行了实例化对象的操作,所以导致实例化了两个对象。
为了通过加锁实现线程同步,可以给getInstance()方法加一把锁,用 synchronized 来修饰方法。
public synchronized static MyThread getInstance(){
if (instance == null){
instance = new MyThread();
}
return instance;
}
// 输出
creatMyThread...
synchronized 可以修饰方法,也可以修饰代码块。接下来,通过同步代码块的方式来实现单例模式。
public class MyThread extends Thread{
private volatile static MyThread instance;
private MyThread(){
System.out.println("creatMyThread...");
}
public static MyThread getInstance(){
synchronized (MyThread.class){
if (instance == null){
instance = new MyThread();
}
}
return instance;
}
}
// 输出
creatMyThread...
这里使用了 volatile 关键字修饰instance,volatile的作用是可以使内存中的数据对线程可见,这句话是什么意思呢?首先要说明一下Java的内存模型,一个线程在访问内存数据时,其实不是拿到该数据本身,而是将该数据复制保存到工作内存中。相当于取出一个副本,对工作内存中的数据进行修改,再保存到主内存中,即主内存对线程是不可见的。所以当线程1拿到锁,并锁定整个类之后,就实例化了instance对象,即“instance = new MyThread();”。但是此时的instance是工作内存中的数据,还需要将工作内存中的数据保存到主内存中。然而锁定的只是实例化的步骤,保存到主内存的步骤没有加锁。所以工作内存中的instance完成实例化之后,还未更新到主内存之前就释放了锁。线程2立即获取锁,又从主内存复制了一份数据,此时的数据还是null。线程2又在工作内存中完成了一次实例化,然后线程1和线程2再将它们各自实例化之后的数据保存到主内存中。
死锁
使用 synchronized 可以实现线程同步,这可以解决多线程并行访问数据带来的安全问题,但是也会带来一个问题 – 死锁。
死锁的意思是每个线程都占用一个资源并且不愿意释放,而且任意一个线程想继续执行就必须获取其他线程的资源,那么所有的线程都处于阻塞状态,程序无法向下执行也无法结束。
破解死锁的方法是:唯有某个线程愿意作出让步,贡献出自己的资源给其他线程使用,获取到资源的线程就可以执行自己的业务方法,执行完毕后会释放它所占用的两个资源,其他线程就可以依次获取资源来执行业务方法。
????
重入锁
重入锁(ReentrantLock)是对 synchronized 的升级,synchronized 是通过JVM实现的,ReentrantLock是通过JDK实现的。重入锁指可以给同一个资源添加多个锁,并且解锁的方式与synchronized也有不同。synchronized 的锁是线程执行完毕之后会自动释放的,ReentrantLock的锁必须手动释放,可以通过ReentrantLock实现访问量统计。
public class MyThread extends Thread{
private static int num = 0;
private ReentrantLock reentrantLock = new ReentrantLock();
public void run(){
reentrantLock.lock();
num++;
System.out.println(Thread.currentThread().getName() + "是当前的第" + num + "个访问者!");
reentrantLock.unlock();
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread, "线程1");
Thread t2 = new Thread(myThread, "线程2");
t1.start();
t2.start();
}
}
// 输出
线程1是当前的第1个访问者!
线程2是当前的第2个访问者!
首先需要实例化 ReentrantLock 的成员变量,在业务方法中需要加锁的地方直接调用对象的 lock()方法即可,同理需要解锁的地方直接调用对象的 unlock()方法即可。使用重入锁的时候需要注意加了几把锁就必须释放几把锁
ReentrantLock除了可重入之外,还有一个可中断的特点,可中断是指某个线程在等待获取锁的过程中可主动终止线程,通过调用对象的lockInterruptibly()来实现。
ReentrantLock还具备限时性的特点,指可以判断某个线程在一定的时间内能否获取锁,通过调用tryLock(long timeout, TimeUnit unit)方法来实现。
public class MyRunnable implements Runnable{
public ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
try {
if (reentrantLock.tryLock(3, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+ "get lock");
Thread.currentThread().sleep(5000);
}
else {
System.out.println(Thread.currentThread().getName() + "not lock");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (reentrantLock.isHeldByCurrentThread()){
reentrantLock.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable,"线程1").start();
new Thread(myRunnable,"线程2").start();
}
}
// 输出
线程1get lock
线程2not lock
线程1和线程2并行访问,业务方法的执行需要5000ms。reentrantLock.tryLock(3,TimeUnit.SECONDS)表示如果线程启动之后的3s内该线程没有拿到锁,则返回false,反之返回true。很显然5000ms大于3s,会有一个线程是肯定拿不到锁的。