文章目录
一、什么是多线程
就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个进程。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
注意:
- 很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只执行一个代码,因为切换得很快,所以就有同时执行的错觉。
- 程序、进程和线程的大家可以通过百度搜索得知,在这里我就不多阐述
二、为什么要用多线程
- 为了更好的利用CPU的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待
- 进程之间不能共享数据,线程可以
- 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小
- Java语言内置了多线程功能支持,简化了java多线程编程
三、一个线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
由图可知一共有五种状态:
新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
四、创建线程的方法
1、继承Thread类
步骤:
- 定义类继承Thread
- 复写Thread类中的run方法
目的:将自定义代码存储在run方法,让线程运行
-== 调用线程的start方法==
该方法有两步:启动线程,调用run方法
代码如下:
package com.kuang.demo01;
public class TestThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("--i");
}
}
public static void main(String[] args) {
TestThread1 testThread1= new TestThread1();
testThread1.start();
for (int i = 0; i < 2000; i++) {
System.out.println("i--");
}
}
}
2、实现Runnable接口
接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。
步骤:
- 定义类实现Runnable接口
- 覆盖Runnable接口中的run方法
将线程要运行的代码放在该run方法中 - 通过Thread类建立线程对象
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象 - 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
代码如下:
package com.kuang.demo01;
//创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread3 implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 200; i++) {
System.out.println("我在看代码----" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
TestThread3 testThread3 = new TestThread3();
//创建线程对象,通过线程对象来开启我们的线程,代理
// Thread thread = new Thread(testThread3);
//
// thread.start();
new Thread(testThread3).start();
for (int i = 0; i < 1000; i++) {
System.out.println("我在学习多线程---" + i);
}
}
}
3、通过Callable创建线程
步骤:
- 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值
- 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象启动新线程
- 调用FutureTask对象的get()方法获取子线程执行结束后的返回值
代码如下:
package com.gaoji;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//实现callable接口
class Callable implements java.util.concurrent.Callable<Integer> {
@Override
public Integer call() throws Exception {
return 100;
}
}
public class ThreadNer{
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable());
new Thread(futureTask).start();
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
四、线程状态管理
1、线程睡眠—sleep
线程睡眠的原因:线程执行的太快,或需要强制执行到下一个线程。
线程睡眠的方法(两个):
-
sleep(long millis)在指定的毫秒数内让正在执行的线程休眠
-
sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠
代码如下:
package com.state;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;
public class TestSleep2 {
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
while(true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());//更新当前时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//模拟倒计时
public void tenDown() throws InterruptedException{
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num <= 0){
break;
}
}
}
}
2、线程让步—yield
该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。
代码如下:
package com.state;
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束执行");
}
}
public class TestYield{
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
sleep和yield的区别:
- sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获
- sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态
3、线程合并—join
当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。
join可以用来临时加入线程执行。
代码如下:
package com.state;
//测试join方法
//想象为插队
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程VIP来了" + i);
}
}
public static void main(String[] args) {
//启动我们的线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 500; i++) {
if(i == 200){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main" + i);
}
}
}
4、停止线程
原stop方法因有缺陷已经停用了,那么现在改如何停止线程?现在分享一种,就是让run方法结束。
开启多线程运行,运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
代码如下:
package com.state;
import com.kuang.demo02.TestCallable;
//测试stop
//1.建议线程正常停止-->利用次数,不建议死循环
//2.建议使用标志位-->设置一个标志位
//3.不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class TestStop implements Runnable{
//1.设置一个标识位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
System.out.println("run....Thread" + i++);
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
//调用stop方法切换标志位,让线程停止
if(i == 900){
testStop.stop();
System.out.println("线程停止了");
}
}
}
}
特殊情况:当线程处于了冻结状态,就不会读取到标记,也就不会结束。当没有指定方法让冻结的线程回复到运行状态时,我们需要对冻结状态进行清除,也就是强制让线程恢复到运行状态中来,这样可就可以操作标记让线程结束。
Thread类提供该方法: interrupt();(如果线程在调用Object类的wait()、wait(long)、wait(long,int)方法,或者该类的join()、join(long)、join(long、int)、sleep(long)或sleep(long、int)方法过程中受阻,则其中断状态将被清除,还将收到一个InterruptedException。)
5、设置优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
** 代码如下:**
package com.state;
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
六、线程同步与锁
1、为什么要进行线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。
2、不同步会发生的问题
在介绍同步方法之前先演示一下当多个线程操作一个共享资源时可能会发生的错误,这里用的方法是让线程在执行时睡眠10毫秒,会导致多个线程去操作同一个资源变量。
** 代码如下:**
package com.gaoji;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TestLock2 t1 =new TestLock2();
new Thread(t1).start();
new Thread(t1).start();
new Thread(t1).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
lock.lock();
if(ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
同步方法1:
同步函数:就是用synchronize关键字修饰的方法。因为每个java对象都有一个内置锁,当用synchronize关键字修饰方法时内置锁会保护整个方法,而在调用该方法之前,要先获得内置锁,否则就会处于阻塞状态。
public synchronized void run() {
}
同步方法2:
同步代码块:就是拥有synchronize关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public void run() {
while(true){
synchronized (this) { //同步代码块
if(tick>0){
try {
Thread.sleep(10); //执行中让线程睡眠10毫秒,
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + tick--);
}
}
}
}
七、死锁
进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
代码如下:
package com.gaoji;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TestLock2 t1 =new TestLock2();
new Thread(t1).start();
new Thread(t1).start();
new Thread(t1).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
lock.lock();
if(ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}