多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

时间:2023-12-28 16:30:56

1概念

1.1进程

进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

任务管理器中:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

1.2线程

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1.3多线程

就是一个程序中有多个线程在同时执行。

单线程程序:多个任务只能依次执行。只有一个线程, 例:main方法中调用多个方法,效率较低

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程程序:多个任务可以同时执行。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

1.4程序运行原理

分时调度

所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

实际上,CPU(*处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1.5主线程

程序从上往下执行的过程:

javac编译----JVM运行Demo01----找到main方法,运行----找到操作系统OS,开启线程

对于CPU就有了一条执行路径,运行方法main的这条路径就叫main,即主线程。

例:

public class Demo01 {
//主线程
public static void main(String[] args) {
method01();
System.out.println("-9的绝对值是:"+Math.abs(-9));
}
public static void method01(){
for(int i=0;i<10;i++){
System.out.println(i);
}
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

加一条错误语句:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

可以看到异常发生在main线程中

2 Thread类

Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

常用方法:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

创建新执行线程有两种方法:

2.1方法1

定义Thread子类,继承Thread,重写run方法

在测试类中创建Thread子类对象

子类对象调用start方法,并且只能调用一次

例:

public class ThreadDemo extends Thread{
public void run() {
for(int i=0;i<10;i++){
System.out.println("Thread:"+i);
}
}
}
public class Test01 {
public static void main(String[] args) {
ThreadDemo td=new ThreadDemo();
td.start();
for(int i=0;i<10;i++){
System.out.println("main:"+i);
}
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

start()方法的两个任务:

1)让线程执行

2)让JVM调用线程中的run方法

内存图:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

图说明:

Main进栈

看到New Thread,start(),会开线程,会再开一个新的栈,run方法进入新栈

两个栈就是两个线程

每new一个Thread,就多一个栈

注意:要执行start方法,不是run方法,

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

这就不是多线程了,是正常调用方法。

2.2获取线程名称

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

例:

public class ThreadDemo extends Thread{
public void run() {
System.out.println("线程名为:"+getName());
}
}
public class Test02 {
public static void main(String[] args) {
ThreadDemo td=new ThreadDemo();
td.run();
Thread td2=Thread.currentThread();
System.out.println("主线程名为:"+td2.getName());
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

注意:主线程的名字只能用Thread.currentThread()这种方式获取,如果这样:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

这里因为main方法是static,而getName()不是static,静态不能调用非静态,所以即使Test03继承了Thread也不能直接用getName()。

2.3修改线程名称(了解即可,尽量不要修改,没有必要)

1)setName()方法

public class ThreadDemo extends Thread{
public void run() {
System.out.println(getName());
}
}
public class Test03 extends Thread{
public static void main(String[] args) {
ThreadDemo td=new ThreadDemo();
td.setName("改名1");
td.run();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

2)构造方法

public class ThreadDemo extends Thread{
public ThreadDemo(){ }
public ThreadDemo(String name){
super(name);
} public void run() {
System.out.println(getName());
}
}
public class Test03 extends Thread{
public static void main(String[] args) {
ThreadDemo td=new ThreadDemo("改名2");
td.run();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

Tips:主线程无法改名,就叫main

2.4 sleep()方法

public class ThreadDemo extends Thread{
public void run() {
for(int i=0;i<5;i++){
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(i);
}
}
} 
public class Test03 extends Thread{
public static void main(String[] args) {
ThreadDemo td=new ThreadDemo();
td.run();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

3创建线程方式2—实现Runnable接口

3.1步骤

  1. 创建实现类,实现Runnable接口,重写run方法
  2. 在测试类中创建实现类对象,创建线程对象
  3. 将实现类对象传入线程对象的构造方法
  4. 用线程对象开启线程

例:

public class MyRunnable implements Runnable{
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Demo01 {
public static void main(String[] args) {
//创建线程任务对象
MyRunnable mr=new MyRunnable();
//创建线程对象1
Thread t1=new Thread(mr);
//开启线程1
t1.start(); //创建线程对象2
Thread t2=new Thread(mr);
//开启线程2
t2.start();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

3.2好处

实现Runnable接口,避免了继承Thread类的单继承局限性,较为常用

实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。

继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

总结:把线程任务的定义,和线程的创建,分开了

高内聚,低耦合

3.3线程的匿名内部类使用

public class Demo02 {
public static void main(String[] args) {
Runnable r=new Runnable(){
public void run() {
System.out.println("重写后的run方法");
}
};
//创建线程
Thread t=new Thread(r);
//开启线程
t.start();
}
}

简写:

public class Demo2 {
public static void main(String[] args) {
new Thread(new Runnable(){
public void run() {
System.out.println("重写后的run方法");
}
}).start();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

4线程的状态

在Thread类中有个内部类:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

线程状态。线程可以处于下列状态之一:

NEW
至今尚未启动的线程处于这种状态。

RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。

BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。

WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。

TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。

TERMINATED
已退出的线程处于这种状态。

图示:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

5线程池

5.1概念

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

5.2使用线程池方式--Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

1)Executors:线程池创建工厂类

public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象

2)ExecutorService:线程池类

Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

3)Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  创建线程池对象

  创建Runnable接口子类对象

  提交Runnable接口子类对象

  关闭线程池

例:

public class MyRunnable implements Runnable{
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class Demo03 {
public static void main(String[] args) {
//从线程池工厂获取线程池对象
ExecutorService es=Executors.newFixedThreadPool(2); //2条线程的线程池
//线程池中抽取一个有空的线程执行线程任务
es.submit(new MyRunnable());
es.submit(new MyRunnable());
//销毁线程池
es.shutdown();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

5.3使用线程池方式—Callable接口

1)Callable接口:

  与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

2)ExecutorService:线程池类

  <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的call()方法

3)Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  创建线程池对象

  创建Callable接口子类对象

  提交Callable接口子类对象

  关闭线程池

例:

public class MyCallable implements Callable<String>{
public String call() throws Exception {
return "abc";
}
} 
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; public class Demo01 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//从线程池工厂获取线程池对象
ExecutorService es=Executors.newFixedThreadPool(2);
//获取call方法执行后的Future对象
Future<String> str=es.submit(new MyCallable());
//从Future对象中获取返回值
String s=str.get();
System.out.println(s);
//销毁线程池
es.shutdown();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

5.4练习:从1到100的和,从1到200的和

import java.util.concurrent.Callable;

public class CallSum implements Callable<Integer>{
//定义成员变量
private int num; //构造方法
public CallSum(){
};
public CallSum(int num){
this.num=num;
}; //线程任务
public Integer call() throws Exception {
int sum=0;
for(int i=0;i<=num;i++){
sum+=i;
}
return sum;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService es=Executors.newFixedThreadPool(2); Future<Integer> f=es.submit(new CallSum(100));
System.out.println(f.get()); Future<Integer> f2=es.submit(new CallSum(200));
System.out.println(f2.get()); es.shutdown();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

6 线程安全

6.1线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

例:电影院卖票

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

代码示例:

public class MyTicker implements Runnable{
private int ticket=100; //100张票
//线程任务
public void run() {
while(true){
if(ticket>0){
try {
Thread.sleep(10); //模拟资源被抢占
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"出售第"+ticket--+"张票");
}
}
}
}
public class SaleTicket {
public static void main(String[] args) {
//创建线程任务
MyTicker mt=new MyTicker();
//创建线程
Thread t0=new Thread(mt);
Thread t1=new Thread(mt);
Thread t2=new Thread(mt);
//开启线程
t0.start();
t1.start();
t2.start();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

在多线程访问同一个资源的时候,往往会出现安全问题。虽然不是一定发生,但是只要可能发生,这就是多线程的安全隐患,是不允许的。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

6.2线程同步

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  方式1:同步代码块

  方式2:同步方法

关键字:Synchronized(同步)

6.2.1同步代码块

  在代码块声明上 加上synchronized

  synchronized (锁对象) {

    可能会产生线程安全问题的代码

  }

注意:

1)同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

2)把对共享数据的操作括起来

3)锁对象:定义成成员变量

修改后:

public class MyTicker implements Runnable{
private int ticket=100; //100张票
private Object obj=new Object(); //锁对象
//线程任务
public void run() {
while(true){
synchronized (obj) {
if(ticket>0){
try {
Thread.sleep(10); //模拟资源被抢占
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"出售第"+ticket--+"张票");
}
}
}
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

原理图:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

当t0进来时,不管是否休眠,其他线程都不能进来,当一个线程进入数据操作时,无论是否休眠,其他线程只能等待。

6.2.2同步方法

在方法声明上加上synchronized

  public synchronized void method(){

  可能会产生线程安全问题的代码

  }

同步方法中的锁对象是 this

public class MyTicker2 implements Runnable {
private int ticket = 100; // 100张票 public synchronized void sale(){
//可能会产生线程安全的代码
if (ticket > 0) {
try {
Thread.sleep(10); // 模拟资源被抢占
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售第" + ticket-- + "张票");
}
} // 线程任务
public void run() {
while (true) {
sale();
}
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

静态同步方法: 在方法声明上加上static synchronized

  public static synchronized void method(){

    可能会产生线程安全问题的代码

  }

注意:

静态同步方法中的锁对象是 类名.class(本类字节码对象,以后学反射再了解)

静态不能访问非静态,所以要把成员变量也加上static

Tips:

1)同步怎么保证安全性?

  没有锁的线程不能执行只能等待

2)加了同步,线程进入同步判断锁时,获取锁,释放锁,会导致运行速度下降

3)StringBuffer里面所有方法都加了关键字,所以慢,但安全

  StringBuilder没有加,所以快,但不安全

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

总结:

单线程不会出现安全问题

只有多个线程共享同一个资源时,才会出现安全问题

6.3 Lock接口

Jdk1.5以后出现的用来替代synchronized

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

常用方法:

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

没有static,所以要用子类对象调用

锁对象就是Lock对象

private Lock lock=new ReentrantLock();

例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class MyTicker3 implements Runnable {
private int ticket = 100; // 100张票
// 创建Lock接口实现类对象
private Lock lock = new ReentrantLock(); // 线程任务
public void run() {
while (true) {
// 获取锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售第" + ticket-- + "张票");
}
// 释放锁
lock.unlock();
}
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

6.4死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

死锁,两个线程互相等待

前提:必须是多线程之间出现的同步嵌套

代码示例:

public class LockA {
private LockA(){ }
public final static LockA locka=new LockA();
}
public class LockB {
private LockB(){ }
public final static LockB lockb=new LockB();
}
public class DeadLock implements Runnable{
private int i=0;
public void run(){
while(true){
if(i%2==0){
//先进A同步,再进B同步
synchronized (LockA.locka) {
System.out.println("if...locka");
synchronized (LockB.lockb) {
System.out.println("if...lockb");
}
}
}else{
//先进B同步,再进A同步
synchronized (LockB.lockb) {
System.out.println("else...lockb");
synchronized (LockA.locka) {
System.out.println("else...locka");
}
}
}
i++;
}
}
}
public class Demo01 {
public static void main(String[] args) {
DeadLock dl=new DeadLock();
Thread t0=new Thread(dl);
Thread t1=new Thread(dl);
t0.start();
t1.start();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

死锁无法解决,只能避免。

6.5等待唤醒机制

6.5.1定义

线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

6.5.2等待唤醒机制所涉及到的方法

wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。

notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。

notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

注意:

1)所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。

2)同时这些方法在使用时必须标明所属锁(锁对象调用方法),这样才可以明确出这些方法操作的到底是哪个锁上的线程。

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

代码示例:

public class Resource {
public String name; //为了演示方便,设为public
public int age;
//添加标记:true赋值完成,false输出完成
public boolean flag=false;
}
public class Input implements Runnable{
private Resource r;
public Input(){ }
public Input(Resource r){
this.r=r;
} //给Resource赋值
public void run() {
int i=0;
while(true){
//添加同步代码块
synchronized (r) {
//判断标记
if(r.flag){
try {
r.wait(); //要用锁调用
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(i%2==0){ //模拟交替进行,用奇偶数实现
r.name="张三";
r.age=18;
}else{
r.name="李四";
r.age=81;
}
r.flag=true; //赋值完成
r.notify(); //唤醒
}
i++;
}
}
}
public class Output implements Runnable{
private Resource r;
public Output(){ }
public Output(Resource r){
this.r=r;
} //对Resource输出
public void run() {
while(true){
//添加同步代码块
synchronized (r) {
//判断标记
if(!r.flag){
try {
r.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(r.name+"..."+r.age);
r.flag=false; //输出完成
r.notify(); //唤醒
}
}
}
}
public class Demo01 {
public static void main(String[] args) {
Resource r=new Resource();
Input in=new Input(r);
Output out=new Output(r);
Thread tin=new Thread(in);
Thread tout=new Thread(out);
tin.start();
tout.start();
}
}

多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全

注意:

1)要共享同一个资源,所以用构造方法,赋同一个对象r

2)Wait()和notify()要放在同步中,要用锁对象调用

3)实现交替进行:

  用一个标记flag:

  true:赋值完成

  false:输出完成

  Input:如果是true,等待;如果是false,赋值,将flag改为true

  Output:如果是true,输出,将flag改为false;如果是false,等待

总结:

线程池:掌握

线程安全:了解