本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
什么是多线程?
首先操作系统有一种能力叫多任务,看起来可以在同一时间运行多个程序,实际上操作系统会为每个进程分配CPU时间片,给人并行处理的感觉。
多线程在更低一层扩展了多任务的概念:单个程序看起来在同时完成多个任务,每个任务在一个线程中执行,线程是控制线程的简称。如果一个程序可以同时运行多个线程,则称这个程序是多线程的--《java核心技术》
可以举一个例子:比如一个同学在学习的时候三心二意想着去玩游戏但是他的父母又要求他一定要学习,那么这个同学在一边学习一边想玩游戏的时候就是在实现“多线程” 用代码来实现就是
public class LearnTime {
public static void main(String[] args) {
new play().start(); // 开启多线程
for (int i = 0; i < 100; i++) {
System.out.println("我要学习!!!");
}
}
}
class play extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我想玩游戏");
}
}
}
在运行结果中会出现这样的情况:
这样就是简单的一个多线程程序
线程的创建方式
1. Runnable接口
2. 继承Thear类实现
3. 实现Callable接口
1.实现Runnable接口
public class TestRunnable {
public static void main(String[] args) {
// 第三步:开启线程
new Thread( new Learn()).start();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程");
}
}
}
/**
* 第一步: Learn 实现 Runnable 接口
**/
class Learn implements Runnable{
/**
* 第二步:重写run方法
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是子线程!!");
}
}
}
由于Runnable是一个函数式接口(只有一个方法),可以使用lambda表达式创建实例
public class TestRunnable {
public static void main(String[] args) {
Runnable Learn = () ->{
for (int i = 0; i < 20; i++) {
System.out.println("我是子线程");
}
};
new Thread(Learn).start();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程");
}
}
}
2.继承Thread类
public class TextThread {
public static void main(String[] args) throws InterruptedException {
// 第三步:创建 test1对象使用start对象
ThreadNew1 test1 = new ThreadNew1();
test1.start();
for (int i = 0; i < 20; i++) {
System.out.println("玩耍第" + i+"天");
if (i == 30) {
test1.join();
}
}
}
}
/**
* 第一步:继承Thread类
**/
class ThreadNew1 extends Thread {
// 第二步:重写run方法
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("学习第" + i + "天!");
}
}
}
那么前面两种方法有什么不同呢?
首先要看Thread类和Runnable接口的关系
从源码来看其实很容易就可以发现Thread类实现了Runnable接口 Thread类中也实现了run方法。
所以继承Thread类的类重写run方法就相当于间接实现了Runnable接口的run方法、又由于继承了thread类,所以可以使用父类thread类中的方法,但是这样的话你所创建的类就只可以由一个线程使用。
那么要如何使你创建的类可以给多个线程调用呢?
实现Runnable类交给Thread类代理就可以解决这个问题 ---> 方法一
public class TestRunnable {
public static void main(String[] args) {
// 第三步:开启线程
Thread thread1 = new Thread( new Learn());
thread1.start();
Thread thread2 = new Thread(new Learn());
thread2.start();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程");
}
}
}
/**
* 第一步: Learn 实现 Runnable 接口
**/
class Learn implements Runnable{
/**
* 第二步:重写run方法
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是子线程" + Thread.currentThread().getName());
}
}
}
运行结果如图: \
3.实现Callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class LearnCallable {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
new Thread(futureTask).start();
try{
Integer integer = futureTask.get();
}catch (InternalError e){
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
}
}
// 3、实现callable接口
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("我是实现了Callable接口的类");
return 50;
}
}
与Runnable接口最大的不同就是Callable接口可以返回一个值
线程状态
线程可以有如下6种状态:
New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(终止)
New(新建状态)
用new操作符创建一个新的进程时,如new Thread(r),新建了一个进程还没有开始运行,这个意味着这个状态是新建(New),它还需要进行一下基础工作要做。
Runnable(可运行状态)
一旦调用start方法,线程就处于可运行状态。一个可运行的线程可能在运行也可能不在运行。(要交由cpu进行调用)
Blocked(阻塞)和Waiting(等待)状态
- 当线程处于阻塞或者等待状态时,它是不活动的,它不允许运行代码,而且消耗最少的资源。要由线程调度器重新激活这个线程。
- 当一个线程需要获得一个锁而这个锁被其它线程占用,这个线程就会因为得不到锁而变为阻塞状态。
- 当线程等待另一个线程通知调度器出现一个条件时,这个线程就会进入等待状态。
- 有的方法存在超时参数,调用这些方法会进入计时等待状态,保存到超时期满或者等到合适的通知。
Terminated(终止状态)
- run方法正常退出,线程自然终止
- 因为一个无法捕获的异常终止了run方法,使得线程意外终止
3. 可以调用stop方法杀死一个线程,该方法会抛出一个ThreadDeath错误对象,这会杀死线程。stop方法已经被废弃。
相关API
java.lang.Thread
static void yield
使当前正在执行的线程向另一个线程交出运行权。这是一个静态方法
void join
等待终止指定的线程
void join(long millis)
等待终止指定的线程或者等待经过的毫秒数
Thread.State getState()
得到这个线程的状态:取值为New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(终止)
图解:
tatic void yield
void join
线程属性
中断线程
因为之前说到的stop方法被废置,现在没有办法可以强制线程终止,但是现在存在interrupt方法可以用来请求终止一个进程。当对一个线程调用interrupt方法时,就会设置线程的中断状态。这个是每个线程都有的boolean标识。每一个线程都要时刻监视这个标识。
静态的Thread.currentThead方法获得当前线程,然后调用isInterrupted方法判断是否为中断状态
值的注意的是如果线程被堵塞,就无法检测中断状态。这里要引入InterruptedException(中断异常)。
当在一个被sleep或者wait调用阻塞的线程上调用interrupt方法时,那个阻塞调用(sleep或者wait)会被一个InterruptedException异常中断。(但是有一些阻塞IO调用不能被中断)
在普遍情况下,中断将会被解释为一个终止请求
相关调用API
java.lang.Thread
void interrupt
向线程发送中断请求。将线程的中断状态设置为true。如果该线程被sleep调用阻塞则抛出一个InterruptedException异常
static boolean interrupt
测试当前线程是否被中断(正在执行这个指令的线程)。注意这个是一个静态方法,而且有一个副作用--它将当前线程的中断状态重置为false
boolearn isInterrupted
测试当前线程是否被中断(正在执行这个指令的线程)。不改变中断状态
static Thread currentThread()
返回表示当前正在执行的线程的Thread对象\
守护线程
守护线程的唯一作用是为其它线程提供服务,虚拟机不会等待守护线程的终止
可以使用 void setDaemon(boolean isDaermon)标识该线程为守护线程。这一方法必须在线程启动前调用
线程优先级
可以使用setPriority方法设置一个线程的优先级,默认为5,最小为1,最大为10,但是线程优先级高度依赖于操作系统。在一些操作系统中,一些优先级可能会合并
同步(这是一个非常非常重要的东西)
竞态条件:多个线程需要修改同一数据,线程可能会相互覆盖。这要取决于线程访问数据的次序,可能会导致对象被破坏。
为了避免这种情况发生就有了新的概念----锁
锁对象
有两种机制可以避免并发访问代码块。显式锁和隐式锁
显式锁
Java5引入了ReentrantLock类。使用方法如下
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
Lollipop lollipop = new lollipop();
Runnable A = new Runnable() {
@Override
public void run() {
lollipop.setNum(5,false);
}
};
Runnable B = new Runnable() {
@Override
public void run() {
lollipop.setNum(6,true);
}
};
}
}
class Lollipop {
private ReentrantLock newLock = new ReentrantLock();
private int num = 10;
public void setNum (int i , boolean flag){
newLock.lock();//上锁
try{
if (flag){
this.num += i;
}else {
this.num -= i;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
newLock.unlock();//开锁
}
}
}
要把unlock操作放在finally语句中,否则当意外终止线程时锁不会释放,导致其他线程一直堵塞
条件对象
可以使用一个条件对象来管理那些已经获得了一个锁但是不能工作的线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
Lollipop lollipop = new Lollipop();
Runnable A = new Runnable() {
@Override
public void run() {
lollipop.setNum(5,false);
}
};
Runnable B = new Runnable() {
@Override
public void run() {
lollipop.setNum(6,true);
}
};
}
}
class Lollipop {
private ReentrantLock newLock = new ReentrantLock();
private Condition condition;
private int num = 10;
public void setNum (int i , boolean flag){
newLock.lock();//上锁
try{
if (flag){
this.num += i;
condition.signalAll(); // 增加棒棒糖后重新进行拿棒棒糖线程
}else {
while (this.num - i < 0){
condition.await(); // 暂停活动状态,释放锁等待增加棒棒糖的线程
}
this.num -= i;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
newLock.unlock();//开锁
}
}
}
当调用signalAll方法后线程应当再次检测是否满足条件---signalAll方法仅仅只是通知等待的线程,现在有可能满足条件,值得再次检测,并且signalAll调用不会立刻激活一个等待的线程。它只是解除等待线程的阻塞状态,使得这些线程可以在当前线程释放锁后重新竞争访问对象
隐式锁
先对锁和条件进行一个总结:
- 锁用来保护代码片段,一次只能有一个线程可以执行被保护的代码
- 锁可以管理试图进入被保护代码的线程
- 一个锁可以有一个或者多个相关连的条件对象
- 每个条件对象管理那些已经进入被保护代码片段但是还不能运行的线程
synchronized关键字 从Java1开始Java每个对象都有一个内部锁。如果一个方法声明有synchronized关键字,那么对象的锁将保护整个方法。也就是说要访问这个方法要获得内部对象锁
public class TestRunnable {
public static void main(String[] args) {
// 第三步:开启线程
Thread thread1 = new Thread( new Learn());
thread1.start();
Thread thread2 = new Thread(new Learn());
thread2.start();
Thread.yield();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程");
}
}
}
/**
* 第一步: Learn 实现 Runnable 接口
**/
class Learn implements Runnable{
/**
* 第二步:重写run方法
* 增加synchronized关键字
*/
@Override
public synchronized void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是子线程" + Thread.currentThread().getName());
}
}
}
内部对象锁只有一个关联条件.wait方法将一个线程增加到等待集里,notifyAll/notify方法可以解除等待线程的阻塞
public class TestRunnable {
public static void main(String[] args) {
// 第三步:开启线程
Thread thread1 = new Thread( new Learn());
thread1.start();
Thread thread2 = new Thread(new Learn());
thread2.start();
Thread.yield();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程");
}
}
}
/**
* 第一步: Learn 实现 Runnable 接口
**/
class Learn implements Runnable{
/**
* 第二步:重写run方法
* 增加synchronized关键字
*/
@Override
public synchronized void run() {
for (int i = 0; i < 10; i++) {
while (i >5){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notifyAll();
System.out.println("我是子线程" + Thread.currentThread().getName());
}
}
}
内部对象锁和条件存在一些限制
不能中断一个尝试获得锁的线程
不能指定尝试获得锁的超时时间
每个锁仅有一个条件可能是不够的