基本概念: 程序、进程、线程
- 程序(program): 为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process): 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程(thread): 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
- 简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
单核CPU与多核CPU的理解
- 单核CPU其实是一种假的多线程,因为在一个时间单元内只能执行一个线程的任务。举个不恰当的例子:我在打英雄联盟,同时要在QQ跟两个妹子聊天。游戏是全屏的,所以必须要切屏出去回消息。我就好比是一个单核CPU,在一个时间单元只能执行一个线程任务,所以只能一边打游戏一边切屏出去回复两个妹子(就当是提升游戏难度了),如若游戏里死了(不可能的)或者妹子还没回复我,那我就可以把她们”挂起”(晾着她,等她回我了,再搭理她)。但是由于我切屏回复的速度非常快,所以妹子感觉不出来我在多线操作。
- 如果是多核CPU的话,就用Python写个糊弄学大师,把QQ和脚本挂我云服务器上,这样脚本帮我回复妹子,我可以专心打游戏,我与脚本各司其职。
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发
- 并行:多核cpu运行。多线程时,真正的在同一时刻运行。
- 并发:单核cpu运行多线程时,时间片进行很快的切换,线程轮流执行cpu。(抢票/秒杀活动)
为什么要使用多线程
用多线程只有一个目的,那就是更好的利用CPU的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应”多角色”的程序代码,最起码每个角色要给他一个线程,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的”生产者,消费者模型线程的创建与使用
线程的创建与启动
- Java语言的JVM允许程序运行多个线程,他通过java.lang.Thread类来体现。
-
Thread类的特性
- 每个线程都是通过某个特定的Thread对象的run()方法来完成操作的,通常把run()方法的主体称为线程体
- 通过Thread对象的start()方法而不是run()方法来启动这个线程,而非直接调用run()
Thread类
- Thread():创建新的Thread对象
- Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(String threadname):创建线程并指定线程实例名
- Thread(Runnable target, String threadname):创建新的Thread对象
创建多线程的方式一:继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread的run()方法 —-> 将此线程的方法声明在run()中
- 创建Thread类的子对象
- 通过此对象调用start() 例子:创建一个线程,遍历100以内的所有偶数,创建匿名子类实现
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
//2.重写Thread的run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
}
public class ThreadDemo{
public static void main(String[] args) {
//3.创建Thread类的子对象
MyThread mythread = new MyThread();
//此步骤是将创建的线程命名为"线程1-" 方便观察结果对比
mythread.setName("线程1-");
//4.通过此对象调用start()
mythread.start();
//创建Thread类的匿名子类的方式
new Thread(){
@Override
public void run() {
for (int i = 0; i < 500; i++) {
if(i%10==0){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
}.start();
//将main()命名为主线程-
Thread.currentThread().setName("主线程-");
for (int i = 0; i < 100; i++) {
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
}
部分运行结果:
Thread-170
Thread-180
主线程-5
线程1-36
线程1-38
主线程-7
Thread-190
Thread-1100
···
显而易见,三个线程的运行结果之间存在交叉
例子:创建三个窗口卖票,总票数为100张 (存在线程安全问题,后面会解决)
public class TicketDemo {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1-");
w2.setName("窗口2-");
w3.setName("窗口3-");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread {
//注意这里的static 三个窗口共卖100张票
private static int ticket = 100;
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票号"+ticket--);
}
else break;
}
}
}
部分输出结果:
窗口1-卖票号7
窗口3-卖票号4
窗口2-卖票号5
窗口3-卖票号2
窗口1-卖票号3
窗口2-卖票号1
若在输出语句之前加上sleep(10),让线程进入if判断语句之后休眠10ms,则会导致下面的运行结果,出现重票和错票。 一个线程进入if判断语句之后休眠了,还没有完成自减的操作,此时另一个线程也进来了,一次判断导致了两次自减,所以就会出现了重票和错票的现象。 解决办法就是加个锁(同步监视器),一个线程执行过程中,不让别的线程进来就好了,下文会提供解决办法。
窗口2-卖票号2
窗口3-卖票号2
窗口1-卖票号2
窗口2-卖票号0
窗口3-卖票号1
窗口1-卖票号-1
Thread类的有关方法
- start():启动当前线程,执行当前线程的run()
- run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread(): 静态方法,返回当前代码执行的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前CPU的执行权
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程”睡眠”指定时间的millitime(毫秒)。在指定的millitime毫秒时间内,当前线程是阻塞状态的。
- isAlive():返回boolean,判断线程是否还活着
线程的调度
-
调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
- 同优先级的线程采用先进先出的队列模式,使用时间片策略
- 对于高优先级,使用优先调度的抢占式策略
线程的优先级
-
线程的优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 —->默认优先级
-
涉及的方法
- getPriority() :返回线程优先值
- setPriority(intnewPriority) :改变线程的优先级
-
说明:高优先级的线程要抢占低优先级线程cpu的执行权。
- 但是只是从概率上讲,高优先级的线程高概率的情况下被执行。
- 并不意味着只有当高优先级的线程执行完以后,低优先级的线程才会被执行。
创建多线程的方式二:实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
public class RunnableTicket {
public static void main(String[] args) {
//3. 创建实现类的对象
Window window = new Window();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread w1 = new Thread(window);
Thread w2 = new Thread(window);
Thread w3 = new Thread(window);
w1.setName("窗口1-");
w2.setName("窗口2-");
w3.setName("窗口3-");
//5. 通过Thread类的对象调用start()
w1.start();
w2.start();
w3.start();
}
}
//1. 创建一个实现了Runnable接口的类
class Window implements Runnable {
//这里不需要加static
private int ticket = 100;
//2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票号" + ticket--);
} else break;
}
}
}
部分运行结果:
窗口2-卖票号5
窗口3-卖票号2
窗口1-卖票号3
窗口2-卖票号3
窗口1-卖票号0
窗口3-卖票号-1
窗口2-卖票号1
继承方式和实现方式的联系与区别
-
比较上述两种创建线程的方式
- 开发中:优先选择实现Runnable接口的方式
- 原因:没有类的单继承性的局限性,实现方式更适合来处理多个线程共享数据的情况。
- 联系:Thread类也实现了Runnable接口 -> public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
线程的生命周期
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的分类
Java中的线程分为两类,一种是守护线程,一种是用户线程- 它们在几乎每个方面都是相同的,唯一的区别是JVM何时离开
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成守护线程
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
同步代码块处理实现Runnable接口的线程安全问题
创建三个窗口卖票,总票数为100张.使用实现RunnabLe接口的方式- 卖票过程中出现重票、错票 —-> 出现了线程的安全问题
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
- 如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程操作完ticket时,其他线程才可以操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
- 在java中,我们通过同步机制,来解决线程的安全问题。
//方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
//同步是一种高开销的操作,因此应该尽量减少同步的内容。
//通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
}
说明:
- 操作共享数据的代码,即为需要被同步的代码—->不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如: ticket就是共享数据
- 同步监视器,俗称:锁。任何一个类的对象,都可以来充当锁。要求:多个线程必须要共用同一把锁。
- 补充:在实现RunnabLe接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
同步代码块处理继承Thread类的线程安全问题
public class TicketDemo {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1-");
w2.setName("窗口2-");
w3.setName("窗口3-");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread {
private static int ticket = 100;
//声明一个静态的obj也可以当锁
// private static Object object = new Object();
@Override
public void run() {
while (true) {
// synchronized (object) {
//注意这里不能用this,此时的this指的是w1,w2,w3三个对象,并不是唯一的
//这里使用的是类锁
synchronized (Window.class) {
if (ticket > 0) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票号" + ticket--);
} else break;
}
}
}
}
部分执行结果:
窗口2-卖票号7
窗口2-卖票号6
窗口2-卖票号5
窗口2-卖票号4
窗口2-卖票号3
窗口2-卖票号2
窗口2-卖票号1
可以看到已经不存在重票和错票的情况了
同步方法处理实现Runnable的线程安全问题
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。---局限性
使用 synchronized关键字,可以修饰普通方法、静态方法,以及语句块。由于java的每个对象都有一个内置锁,
当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
需要注意的是调用静态方法时,锁住的不是对象,锁住的是类。
//修饰普通方法
public synchronized void method(){
}
//修饰静态方法
public static synchronized int increase(){
}
public class RunnableTicket {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window);
Thread w2 = new Thread(window);
Thread w3 = new Thread(window);
w1.setName("窗口1-");
w2.setName("窗口2-");
w3.setName("窗口3-");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (!show())
break;
}
}
//修饰普通方法
private synchronized boolean show() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票号" + ticket--);
return true;
} else return false;
}
}
部分执行结果:
窗口1-卖票号6
窗口1-卖票号5
窗口1-卖票号4
窗口1-卖票号3
窗口1-卖票号2
窗口1-卖票号1
银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。 分析:
- 是否是多线程问题? 是,两个储户线程
- 是否有共享数据? 有,账户(或账户余额)
- 是否有线程安全问题? 有
- 需要考虑如何解决线程安全问题? 同步机制:有三种方式。
class Account {
private double money;
public Account(double money) {
this.money = money;
}
public synchronized void SaveMoney(double cash) {
if (cash > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
money += cash;
System.out.println(Thread.currentThread().getName()+"存入" + cash + "元,当前余额" + money + "元");
}
}
}
class Customer extends Thread {
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.SaveMoney(1000);
}
}
}
public class Test {
public static void main(String[] args) {
Account account = new Account(0);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("客户1-");
c2.setName("客户2-");
c1.start();
c2.start();
}
}
执行结果:
客户1-存入1000.0元,当前余额1000.0元
客户1-存入1000.0元,当前余额2000.0元
客户1-存入1000.0元,当前余额3000.0元
客户2-存入1000.0元,当前余额4000.0元
客户2-存入1000.0元,当前余额5000.0元
客户2-存入1000.0元,当前余额6000.0元
死锁的问题
演示线程的死锁- 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁(两个死傲娇互相暗恋,都在等对方先表白,形成死锁)
- 说明:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续,所以我们使用同步时,要避免出现死锁。
- 解决办法:专门的算法、原则(尽可能避免出现死锁),尽量减少同步资源的定义,尽量避免嵌套同步。
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
上面的例子中,当第一个线程拿到s1锁的时候,休眠了0.1秒的时间内,第二个线程拿到了s2锁。当第一个线程醒了之后,需要拿s2锁,而第二个线程拿着s2锁,又需要拿s1锁,于是两个线程就僵持下去,形成死锁。(加sleep()方法只是增加发生的可能性,不加sleep()方法也有几率会发生死锁)、
Lock锁的方式解决线程安全问题
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- 相同:二者都可以解决线程安全问题
- 不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unLock())
- 优先使用顺序: Lock >> 同步代码块 >> 同步方法
import java.util.concurrent.locks.ReentrantLock;
public class RunnableTicket {
public static void main(String[] args) {
Window window = new Window();
Thread w1 = new Thread(window);
Thread w2 = new Thread(window);
Thread w3 = new Thread(window);
w1.setName("窗口1-");
w2.setName("窗口2-");
w3.setName("窗口3-");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法:lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票号" + ticket--);
} else break;
} finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
线程的通信
线程通信的例子:使用两个线程打E1-100。线程l,线程2交替打印涉及到的三个方法: wait():—旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。 notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。 notifyALL():—旦执行此方法,就会唤醒所有被wait的线程。 说明:
- wait(),notify(),notifyALl()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAlL()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现ILLegaLMonitorstateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.Lang.object类中。
public class ThreadTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程一:");
t2.setName("线程二:");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//2.唤醒被wait的线程,从而达到两个线程交替运行的效果
notify();
if (num <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + num++);
try {
//1.使得调用如下wait()方法的线程进入阻塞状态,并释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else break;
}
}
}
}
sleep()和wait()的异同
相同点
- 一旦执行方法,都可以使得当前的线程进入阻塞状态。 不同点
- 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。 这里可能出现两个问题:- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
- 是否是多线程的问题? 是,生产者的线程,消费者的线程
- 是否有共享数据的问题? 是,店员、产品、产品数
- 如何解决线程的安全问题? 同步机制,有三种方法
- 是否涉及线程的通信? 是
class Clerk {
private int productCount = 0;
public synchronized void InProduction() {
if (productCount < 20) {
System.out.println(Thread.currentThread().getName()+"正在生产第" + (++productCount) + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void SaleProduct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName()+"正在购买第" + (productCount--) + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.InProduction();
}
}
}
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.SaleProduct();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Customer customer = new Customer(clerk);
producer.setName("生产者-");
customer.setName("消费者-");
producer.start();
customer.start();
}
}