Java多线程
章节目录
一、 基本概念
-
并行:多个CPU同时执行多个任务,如:多个人同时做不同的事情。
-
并发:一个CPU(采用时间片)同时执行多个任务。如:多个人做同一件事情。
-
进程(Process):是程序的一次执行过程,或是 正在运行的一个程序。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
线程(Thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运算栈和程序计数器(PC),线程切换的开销小。
一个Java应用程序Java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。(如果发生异常,会影响主线程)
每个线程拥有自己独立的栈和程序计数器;多个线程共享同一个进程中的结构
二、 创建方式
1. 方式一:继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类中run() : 将此线程需要执行的操作生命在该方法中
- 创建Thread类的子类的对象
- 通过此对象调用start()
方式一代码如下(示例):
class SubThread extends Thread {
public void run() {
System.out.println("当前线程名称: " + getName());
}
}
public class ThreadTest {
public static void main(String[] args) {
SubThread s1 = new SubThread();
s1.start();
}
}
2. 方式二:实现Runnable接口
- 创建一个实现Runnable接口的类
- 实现类去实现Runnable中的抽象方法 run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象。
- 通过Thread类的对象调用start()
方式二代码如下(示例):
class SubThread implements Runnable {
@Override
public void run() {
System.out.println("当前线程名称:" + Thread.currentThread().getName());
}
}
public class ThreadTest {
public static void main(String[] args) {
SubThread s = new SubThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
3. 方式三:实现Callable接口
- 创建一个Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在 call( ) 中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象并调用start()
- 利用FutureTask中的 get() 来获取Callable中call方法的返回值
方式三代码如下(示例):
class SubThread3 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;
}
}
public class ThreadTest3 {
public static void main(String[] args) {
SubThread3 s = new SubThread3();
FutureTask futureTask = new FutureTask(s);
new Thread(futureTask).start();
try {
//get()返回值即为FutureTask构造参数Callable实现类重写的call()的返回值.
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
优点:
- call() 可以有返回值
- call() 可以抛出异常,被外面的操作捕获,获取异常信息
- Callable支持泛型
4. 方式四:使用线程池
- 提供指定线程数量的线程池
- 执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象。
- 关闭连接池
注意:
service.execute() 适用于Runnable
service.submit() 适用于Callable
class SubThread4 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": 输出偶数: " + i);
}
}
}
}
class SubThread5 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ": 输出奇数: " + i);
}
}
return null;
}
}
public class ThreadTest4 {
public static void main(String[] args) {
SubThread4 subThread4 = new SubThread4();
SubThread5 subThread5 = new SubThread5();
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(subThread4);
FutureTask futureTask = (FutureTask) service.submit(subThread5);
new Thread(futureTask).start();
try {
Object o = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
service.shutdown();
}
}
三、 线程的使用
1. 测试Thread中的常用方法
- start( ):启动当前线程;调用当前线程的run( )
- run( ): 通常需要重写Thread类中的该方法,将创建的线程要执行的操作声明在此方法中。
- currentThread( ):静态方法,返回执行当前代码的线程
- getName( ): 获取当前线程的名字
- setName( ): 设置当前线程的名字
- yield( ): 释放当前cpu的执行权
- join( ): 在线程a中调用线程b的join( ),此时线程a就会进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop( ): (Deprecated)强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒,在指定的millitime毫秒时间内,当前的线程是阻塞状态。
- isAlive():判断当前线程是否存活。
2. 线程的调度
2.1调度策略
- 时间片:根据时间长短进行替换
- 抢占式:高优先级的线程抢占cpu
2.2调度方法
- 同优先级线程组先进先出队列,使用时间片策略
- 对高校优先级,使用优先调度的抢占式策略
2.3优先级等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
2.4涉及的方法
- getPriority(): 返回线程优先值
- setPriority(int newPriority):改变线程的优先级
线程创建时继承父线程的优先级,低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
四、 生命周期
五、 线程同步
1. 方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:1. 操作共享数据的代码,即为需要被同步的代码。2.共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。 3. 同步监视器,俗称:锁。任何一个类的对象都可以充当锁。
1.同步代码块处理实现Runnable的线程安全问题(示例):
class SubThread2 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 在买票, 票号为: " + tickets);
tickets--;
}else{
break;
}
}
}
}
}
2.同步代码块处理继承Thread类的线程安全问题(示例):
class SubThread1 extends Thread {
private static int tickets = 100;
@Override
public void run() {
while (true) {
synchronized (SubThread1.class) {
if (tickets > 0) {
System.out.println(getName() + ": 在买票, 票号为: " + tickets);
tickets--;
} else {
break;
}
}
}
}
}
2. 方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明成同步的。
- 同步方法仍然涉及到同步监视器,只是不需要显示的声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身(xxx.class)
1.同步方法处理实现Runnable的线程安全问题(示例):
class SubThread2 implements Runnable {
private int tickets = 100;
@Override
public synchronized void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 在买票, 票号为: " + tickets);
tickets--;
}else{
break;
}
}
}
}
2.同步方法处理继承Thread类的线程安全问题(示例):
class SubThread1 extends Thread {
private static int tickets = 100;
@Override
public synchronized void run() {
while (true) {
sale();
}
}
private static synchronized void sale() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ": 在买票, 票号为: " + tickets);
tickets--;
}
}
}
补充:使用同步机制将单例模式中的懒汉式改写为线程安全(示例):
public class Bank {
public Bank() {}
private static Bank instance = null;
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
3. 方式三:Lock(锁)
使用Lock方式解决线程安全问题(示例):
class SubThread2 implements Runnable {
private int tickets = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 在买票, 票号为: " + tickets);
tickets--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
4. synchronized与Lock的对比
- Lock是显示锁(需要手动的开启和关闭)。synchronized是隐式锁,除了作用于自动释放。
- Lock只有代码块锁,synchronized有代码块和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多子类)
5. 线程的死锁问题
- 死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态无法继续。
- 解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
六、 线程通信
1. 涉及到的方法
- wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就会唤醒优先级高的那个线程。
- notifyAll():一旦执行此方法,就会唤醒被wait的所有线程。
2. 适用前提
- wait(), notify(), notifyAll() 这三个方法必须使用在同步代码块或同步方法当中。(此处暂时省略lock的线程通讯方式)
- wait(), notify(), notifyAll() 这三个方法的调用者必须是同步代码块或同步方法当中的同步监视器。否则会出现IllegalMonitorStateException异常
3.sleep( ) 和 wait( ) 的异同
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
- 不同点:
- 两个方法的声明位置不同,Thread类中声明sleep(),Object类中声明wait()。
- 调用的要求不同,sleep() 可以在任何的场景下调用,wait() 必须使用在同步代码块或同步方法中。
- 关于是否释放同步监视器:如果两个方法都是用在同步代码块或同步方法中,sleep() 不会释放同步监视器,wait() 会释放同步监视器。