第五章.多线程
1. 基本概念:程序、进程、线程
1.程序(program)
1.定义:
是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
2.进程(process)
1.定义:
是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
2.说明:
程序是静态的,进程是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
3.线程(thread)
1.定义:
进程可进一步细化为线程,是一个程序内部的一条执行路径。
2.说明:
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
3.优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
4.何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
4.单核CPU和多核CPU的理解
-
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。 -
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
-
一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
5.并行与并发
1.定义:
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
2. 线程的创建和使用
1.多线程的创建
①方式一:
1.创建一个继承于Thread的类的子类
2.重写Thread类的run()方法(将此线程执行的操作声明在run()中)
3.创建Thread类的子类的对象
4.通过此对象调用start()
说明:
start()①启动当前线程②调用当前线程run()
不能通过直接调用run()的方式启动线程
同一个对象不能再启动一个线程( 同一个对象start()只能使用一次)
举例:遍历10以内的所有偶数
package day017;
public class ThreadTest {
public static void main(String[] args) {
//创建Thread类的子类的对象
MyThread m = new MyThread();
//通过此对象调用start()
m.start();
for (int i = 0;i <= 10;i++){
if (i % 2 == 0){
System.out.println(i + "*****main***");
}
}
}
}
//创建一个继承于Thread的类的子类
class MyThread extends Thread{
@Override
//重写Thread类的run()方法(将此线程执行的操作声明在run()中)
public void run() {
for (int i = 0;i <= 10;i++){
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
执行结果:
0*****main***
2*****main***
4*****main***
6*****main***
0
2
4
6
8
10
8*****main***
10*****main***
②方式二(优先选择。原因:没有类的单继承性的局限)
1.定义子类,实现Runnable接口。
2.子类中重写Runnable接口中的run方法。
3. 通过Thread类含参构造器创建线程对象。
4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
举例:
package day017;
public class ThreadTest2 {
public static void main(String[] args) {
Test1 m = new Test1();
Thread t1 = new Thread(m);
t1.start();
}
}
class Test1 implements Runnable{
@Override
public void run() {
for (int i = 0;i <= 100;i++){
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
2.JDK5.0新增线程创建方式
③方式一:实现Callable接口(了解)
1.步骤
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为传递到Thread构造器中,创建Thread的对象
6.Thread的对象调用start()
7.获取call()的返回值
2.Callable比Runnable强大
1.call()可以有返回值的
2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
3.Callable是支持泛型的
3.举例
package day018.Test1;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread t = new NumThread();
//4.将Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask f = new FutureTask(t);
//5.将FutureTask的对象作为传递到Thread构造器中,创建Thread的对象
Thread th1 =new Thread(f);
Thread th2 =new Thread(f);
//6.Thread的对象调用start()
th1.start();
th2.start();
//7.获取call()的返回值
try {
Object sum = f.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
sum += i;
}
}
return sum;
}
}
④方式二使用线程池(了解)
1.了解*
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
1.JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
2.ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
void shutdown() :关闭连接池
3.Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
2.举例
package day018.Test2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadTest {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
Number n1 = new Number();
Thread t1 = new Thread(n1);
Thread t2 = new Thread(n1);
//2.执行指定的线程操作。
service.execute(t1);
service.execute(t2);
//3.关闭线程池
service.shutdown();
}
}
class Number implements Runnable{
@Override
public void run() {
for (int i = 0;i <=100;i++){
if (i % 2 ==0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
3.线程的优先级
-
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 -
如何获取和设置当前线程的优先级
getpriority(): 获取线程的优先级
setpriority(int p): 设置线程的优先级 -
说明:
高优先级要抢低优先级的CPU的执行权,不是先执行完高优先级的在执行低优先级的,而是执行高优先级的概率比较大
4.多线程的常用方法
1.start():启动当前线程,调用当前线程的run()
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():释放当前cpu的执行权
7.join():在线程a中调用线程b的join(),此线程a就进入阻塞状态,直到线程b完全执行完成以后,线程a才结束阻塞状态
8.stop():已过时。当执行此方法时,强制结束当前线程
9.sleep(long millitime):让当前线程睡眠指定的millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态
10.isAlive():判断当前线程是否存活
举例:
package day017;
public class ThreadTest1 {
public static void main(String[] args) {
HelloThread h = new HelloThread("线程二");
h.setName("线程一");
h.start();
Thread.currentThread().setName("主线程");
for(int i = 0;i <= 100;i++){
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// if (i == 40){
// h.yield();
// }
if (i == 20){
try {
h.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(h.isAlive());
}
}
class HelloThread extends Thread{
public HelloThread(String name){
super(name);
}
@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);
}
}
}
}
3.线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。
Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
4.线程的同步
1.线程的安全问题
- 问题:买票过程中,出现了重票,错票(出现了线程的安全问题)
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成,其他线程参与进来,也操作车票
- 如何解决:当一个线程操作车票的过程中,其他线程不能参与进来
2.使用同步机制,解决线程的安全问题
①同步代码块
1.格式
synchronized(同步监视器){
需要被同步的代码
}
2.说明
1.需要被同步的代码:操作共享数据的代码
2.共享数据:多个线程共同操作的变量
3.同步监视器(俗称:锁):任何一个类的对象,都可以充当锁
要求:多个线程必须要共用统一把锁
3.举例
package day018;
public class WindowThreadTest {
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("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private static int ticket = 100;
Object o = new Object();
@Override
public void run() {
//Object o = new Object();错误
while (true) {
synchronized (o) {
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
②同步方法
1.格式:
权限修饰符 synchronized 数据类型 方法名(){
}
publiic synchronized void show(){
}
2.说明:
1.操作共享数据的代码完整的声明在一个方法中,此方法可以声明为同步方法
2.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
3.非静态的同步方法:同步监视器是:this
静态的同步方法:同步监视器是:当前类本身
3.举例
package day018;
public class Test {
public static void main(String[] args) {
W w1 = new W();
W w2 = new W();
W w3 = new W();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class W extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
public static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket);
ticket--;
}
}
}
同步
优点:解决了线程的安全问题
缺点:效率低
3.锁
1.死锁
1.定义:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2.说明:
①出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
②我们写代码要避免死锁
2.使用Lock(锁)解决线程的安全问题
1.格式
class A{
private final ReentrantLock lock = new ReenTrantLock();//实例化ReentrantLock
// private final ReentrantLock lock = new ReenTrantLock(true)表示均分票数,一个接着一个
public void m(){
lock.lock();//调用锁定方法
try{
//保证线程安全的代码;
}
finally{
lock.unlock(); //调用解锁方法
}
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块
2.说明
synchronized 与 Lock 的对比
1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
2. Lock只有代码块锁,synchronized有代码块锁和方法锁
3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)优先使用顺序:
4.Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
3.举例
1.
package day018;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable{
private int ticket = 100;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket);
ticket--;
} else {
break;
}
}finally{
lock.unlock();
}
}
}
}
2.
package day018;
/*
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
*/
import java.util.concurrent.locks.ReentrantLock;
public class LiTest {
public static void main(String[] args) {
Account acc = new Account(0);
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
c1.setName("客户1");
c2.setName("客户2");
c1.start();
c2.start();
}
}
class Account{
private static int money;
public Account(int money){
this.money = money;
}
private final ReentrantLock lock= new ReentrantLock();
public void Add(int m){
lock.lock();
try{
if (m > 0){
money += m;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "余额为:" + money);
}
}finally{
lock.unlock();
}
}
}
class Customer extends Thread{
private Account a;
public Customer(Account a){
this.a = a;
}
@Override
public void run() {
for(int i = 0; i < 3 ;i++){
a.Add(1000);
}
}
}
5.线程的通信
1.涉及到的三个方法
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify():一旦执行此方法,就会唤醒一个被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
2.说明
1.wait(),notify(),notifyAll():三个方法必须使用在同步代码块或同步方法中
2.wait(),notify(),notifyAll():三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则出现异常
3.wait(),notify(),notifyAll():三个方法是定义在java.lang.Object类中
3.举例
/*
使用两个线程打印 1-100。线程1, 线程2 交替打印
*/
package day018.Test1;
public class ThreadTest {
public static void main(String[] args) {
Da d = new Da();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Da implements Runnable {
private int m = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (m > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " : " + m);
m--;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
4.面试题
sleep()和wait()的区别
相同点:
一旦被执行,都可以使当前的线程进入阻塞状态
不同点:
1.声明的位置不同:sleep()声明在Thread中,wait()声明在Object中
2.调用的要求不同:sleep()可以在任何需要的场景下被调用,wait()必须使用在同步代码块或同步方法中
3.关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
6.应用
1.
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处
取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据
package day018.Test1;
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread1 = new Thread(new Productor(clerk));
Thread productorThread2 = new Thread(new Productor(clerk));
Thread consumerThread1 = new Thread(new Consumer(clerk));
productorThread1.start();
productorThread2.start();
consumerThread1.start();
}
}
class Clerk { // 售货员
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了第" + product + "个产品");
notifyAll();
}
}
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" + product + "个产品");
product--;
notifyAll();
}
}
}
class Productor implements Runnable { // 生产者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep( (int)Math.random()*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable { // 消费者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int)Math.random()*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}