Java多线程
Java多线程
线程的理解
程序(programm):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
进程(process):程序的一次执行过程,或是正在运行的一个程序。
- 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
内存结构:

补充:
进程可以细化为多个线程。
每个线程,拥有自己独立的:栈、程序计数器。
多个线程,共享同一个进程中的结构:方法区、堆。
并行与并发
单核CPU与多核CPU的理解:
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发的理解:
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
创建多线程
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性:
Thread类的构造器:
Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
JDK1.5之前创建新执行线程有两种方法:
- 继承Thread类的方式
- 实现Runnable接口的方式
继承Thread类的方式:
- 创建一个类继承于Thread类
- 重写Thread类的run() –> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start():①启动当前线程 ②调用当前线程的run()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package _thread;
import org.junit.Test;
class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); } } } public class ThreadTest{ public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); MyThread myThread1 = new MyThread(); myThread1.start(); for (int i = 0; i < 100; i++) { if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); } }
@Test public void test(){ new Thread(){ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); } } }.start();
new Thread(){ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2!=0) System.out.println(Thread.currentThread().getName()+":"+i); } } }.start(); } }
|
注意:
- 我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
- 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常 IllegalThreadStateException。
实现Runnable接口的方式:
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package _thread;
public class RunnableTest { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr); t1.setName("线程一"); t1.start(); Thread t2 = new Thread(mr); t2.setName("线程二"); t2.start();
} }
class MyRunnable implements Runnable{
@Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }
|
两种线程创建方式的对比:
- 开发中优先选择实现Runnable接口的方式:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程共享数据的情况
- 联系:
public class Thread implements Runnable
- 相同点:
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 目前两种方式启动线程,都是调用的Thread类中的start()。
线程的优先级
线程的优先等级:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
涉及的方法:
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package _thread;
public class ThreadPriorityTest { public static void main(String[] args) { PriorityThread p = new PriorityThread(); p.setPriority(Thread.MAX_PRIORITY); p.start(); Thread.currentThread().setPriority(2); for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority()+":"+i); } } }
class PriorityThread extends Thread{ @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(getName()+":"+getPriority()+":"+i); } } }
|
注意:
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
Thread类常用方法
start():启动当前线程;调用当前线程的run()
run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():释放当前cpu的执行权
join():当某个程序执行流中调用其他线程的 join()方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
stop():已过时。当执行此方法时,强制结束当前线程
sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态
isAlive():判断当前线程是否存活
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package _thread;
public class ThreadMethodTest { public static void main(String[] args) { HelloThread helloThread = new HelloThread(); helloThread.setName("线程一"); helloThread.start();
Thread.currentThread().setName("主线程"); for (int i = 0; i < 100; i++) { if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); if(i==20){ try { helloThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println(helloThread.isAlive()); } }
class HelloThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+i); }
} } public HelloThread(){}
public HelloThread(String name){ super(name); } }
|
补充:
Java中的线程分为两类:一种是守护线程,一种是用户线程。
守护线程是用来服务用户线程的,通过在start()方法前调用 thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直”霸占”着CPU独自运行,所以CPU需要在多条线程之间切换。
图示:

说明:生命周期关注两个概念:状态、相应的方法
- 关注:
- 状态a–>状态b:执行方法(回调方法)
- 主动调用方法:状态a–>状态b
- 阻塞:临时状态,不可以作为最终状态.
- 死亡:最终状态。
线程的同步机制
背景:多个窗口卖票
- 问题:卖票过程中,出现了重票、错票–>出现了线程的安全问题。
- 问题出现的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
- 如何解决:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
java解决方案:在Java中,我们通过同步机制,来解决线程的安全问题。
同步机制的三种方式:①同步代码块 ②同步方法 ③Lock锁
使用的优先顺序:Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)
利弊:
- 同步的方式,解决了线程的安全问题。
- 操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
注意:必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全。
同步代码块
关键字:synchronized
格式:
1 2 3
| synchronized(同步监视器){ }
|
说明:
- 操作共享数据的代码,即为需要被同步的代码。 –>不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要共用同一把锁。
继承Thread类方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package _exer;
public class WindowTicket { public static void main(String[] args) { Window win1 = new Window(); win1.setName("窗口一"); win1.start(); Window win2 = new Window(); win2.setName("窗口二"); win2.start(); Window win3 = new Window(); win3.setName("窗口三"); win3.start(); } } class Window extends Thread{ private static int ticket=100;
@Override public void run() {
while (true){ synchronized (Window.class){
if (ticket > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } }
|
实现Runnable接口方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package _exer;
public class WindowTicket1 { public static void main(String[] args) { Window1 run = new Window1(); Thread win1 = new Thread(run, "窗口一"); win1.start(); Thread win2 = new Thread(run, "窗口二"); win2.start(); Thread win3 = new Thread(run, "窗口三"); win3.start(); } }
class Window1 implements Runnable{ private int ticket=100; @Override public void run() { while (true){ synchronized (this){ if (ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket); ticket--; }else{ break; } } } } }
|
补充:
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
继承Thread类方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package _exer;
public class WindowTicket2 { public static void main(String[] args) { Window2 win1 = new Window2(); win1.setName("窗口一"); win1.start(); Window2 win2 = new Window2(); win2.setName("窗口二"); win2.start(); Window2 win3 = new Window2(); win3.setName("窗口三"); win3.start(); } }
class Window2 extends Thread{ private static int ticket=100; @Override public void run() { while (true){ show(); } } private static synchronized void show(){ if (ticket > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { System.exit(0); } } }
|
实现Runnable接口方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package _exer;
public class WindowTicket3 { public static void main(String[] args) { Window3 run = new Window3(); Thread win1 = new Thread(run, "窗口一"); win1.start(); Thread win2 = new Thread(run, "窗口二"); win2.start(); Thread win3 = new Thread(run, "窗口三"); win3.start(); } }
class Window3 implements Runnable{ private int ticket=100; @Override public void run() { while (true){ show(); } } private synchronized void show(){ if (ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket); ticket--; }else{ System.exit(0); } } }
|
补充:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身(类名.class)
Lock锁
Lock锁实现同步机制是JDK5.0新增的一种方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package _thread;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest { public static void main(String[] args) { Window w = new Window();
Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w);
t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三");
t1.start(); t2.start(); t3.start(); } }
class Window implements Runnable { private int ticket = 100; ReentrantLock lock = new ReentrantLock(); @Override public void run() {
while (true) { try { lock.lock(); if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket); ticket--; }else{ break; } } finally { lock.unlock(); } }
} }
|
关于Lock锁的总结:
- 多个线程必须共用一个ReentrantLock对象
ReentrantLock(boolean fair)传入true可以实现公平策略
面试题:synchronized 与 Lock的异同?
- 相同:二者都可以解决线程安全问题
- 不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
- Lock需要手动的启动同步lock(),同时结束同步也需要手动的实现unlock()。
懒汉式线程安全问题
使用同步机制将单例模式中的懒汉式实现线程安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package _exer;
public class BankTest { }
class Bank{ private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){ if(instance == null){ synchronized (Bank.class) { if(instance == null){ instance = new Bank(); } } } return instance; } }
|
线程死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
- 我们使用同步机制时,要避免出现死锁问题。
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package _thread;
public class DeadLockTest { public static void main(String[] args) { StringBuffer str1 = new StringBuffer(); StringBuffer str2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (str1){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } str1.append("A"); synchronized (str2){ str2.append("1"); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (str2){ str1.append("B"); synchronized (str1){ str2.append("2"); } } } }).start();
System.out.println("输出:"+str1+" "+str2);
}
}
|
线程通信
线程通信涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
- 三个方法必须使用在同步代码块或同步方法中。
- 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现
IllegalMonitorStateException异常。
- 三个方法是定义在
java.lang.Object类中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package _thread;
public class CommunicationTest { public static void main(String[] args) { Number num = new Number(); Thread t1 = new Thread(num); Thread t2 = new Thread(num);
t1.start(); t2.start(); } }
class Number implements Runnable{ private int num=0; @Override public void run() { while (true){ synchronized (this) { notify(); if (num<=100){ System.out.println(Thread.currentThread().getName()+":"+num); num++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } }
|
经典例题:生产者/消费者问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| package _exer;
public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); Consumer c1 = new Consumer(clerk); Consumer c2 = new Consumer(clerk);
p1.setName("生产者1"); c1.setName("消费者1"); c2.setName("消费者2");
p1.start(); c1.start(); c2.start(); } }
class Clerk { private int product = 0;
public synchronized void production() {
if (product < 20) { product++; System.out.println(Thread.currentThread().getName() + ":开始生产第" + product + "个产品"); notifyAll(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
public synchronized void consumption() { if (product > 0) { System.out.println(Thread.currentThread().getName() + ":开始消费第" + product + "个产品"); product--; notifyAll(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
class Producer extends Thread { private Clerk clerk;
public Producer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println(getName()+"开始生产产品......"); while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.production(); } } }
class Consumer extends Thread { private Clerk clerk;
public Consumer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println(getName()+"开始消费产品......"); while (true) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumption(); } } }
|
对于是否释放锁的操作总结:
- 释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
- 不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
JDK5新增线程创建方式
新增方式一:实现Callable接口
与使用Runnable相比,Callable功能更强大些:
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package _thread;
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
public class CallableTest { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask futureTask = new FutureTask(numThread); new Thread(futureTask).start(); try { Object sum = futureTask.get(); System.out.println("偶数和为:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
class NumThread implements Callable{ @Override public Object call() throws Exception { int sum=0; for (int i = 0; i < 100; i++) { if (i%2==0){ System.out.println(i); sum+=i; } } return sum; } }
|
新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没任务时最多保持多长时间后会终止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package _thread;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolTest { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); ThreadPoolExecutor executor=(ThreadPoolExecutor)executorService; executor.setCorePoolSize(15);
executorService.execute(new NumberThread());
executorService.shutdown(); } }
class NumberThread implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(i); } } } }
|