线程

1.1 什么是进程

应用程序的一次运行产生进程。

 

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

为什么存在进程的概念?

 

1.2 什么是线程

参考:https://www.cnblogs.com/geeta/p/9474051.html

 

1.2.1 线程和进程区别

 

 

案例:理解上课的进程

 

1.3 实现多线程

1.3.1 继承Thread

 

package cn.sxt01.thread01;

 

public class MyThread extends Thread {

 

@Override

public void run() {

 

for (int i = 0; i < 10; i++) {

System.out.println("MyThread:" + i);

}

 

}

}

 

package cn.sxt01.thread01;

 

 

public class Test01 {

public static void main(String[] args) {

 

// 1】创建一个线程并执行

MyThread myThread = new MyThread();

myThread.start();

 

// main线程也称主线程

for (int i = 0; i < 10; i++) {

System.out.println("MainThread:" + i);

}

 

}

}

 

 

Test01存在两个线程,一个是main线程,也称主线程。另外一个是myThread线程

两个线程抢占CPU所以程序运行轨迹不确定

 

1.3.2 实现Runnable接口

Runnable接口表示实现类是否具有在多线程中执行的能力。

package cn.sxt01.thread01;

 

public class MyRunnable implements Runnable{

 

@Override

public void run() {

for (int i = 0; i < 10; i++) {

System.out.println("MyRunnable:" + i);

}

}

}

 

案例:卖票

 

package cn.sxt02.thread02;

 

public class TicketThread extends Thread {

 

private static int count = 5;

 

public TicketThread(String name) {

super(name);

}

 

@Override

public void run() {

// 模拟买票

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(super.getName() + "卖出一张,还剩" + count + "");

}

}

}

}

 

窗口A卖出一张,还剩4

窗口B卖出一张,还剩2

窗口C卖出一张,还剩3

窗口B卖出一张,还剩0

窗口A卖出一张,还剩1

 

分析运行轨迹

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(super.getName() + "卖出一张,还剩" + count + "");

}

}

A抢占到CPUcount(5)条件成立,count--,输出

窗口A卖出一张,还剩4

C抢占到CPU

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(super.getName() + "卖出一张,还剩" + count + "");

}

}

C开始执行,执行super.getName() + "卖出一张,还剩" + count + "" 准备好,线程C挂起。

B抢占到CPU

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(super.getName() + "卖出一张,还剩" + count + "");

}

}

B开始执行,指定到输出位置,输出

窗口B卖出一张,还剩2

CPU时间片到,

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(super.getName() + "卖出一张,还剩" + count + "");

}

}

C抢占到CPU,直接冲上次挂起位置开始执行输出

窗口C卖出一张,还剩3

 

结论:

[1]线程在执行过程中,在任意位置时,都有可能被挂起。下次抢占到cpu后,从挂起位置开始执行。

[2]有多个线程访问共享数据时,会出现数据错乱。

[3]多线程提高了cpu利用率同时程序的复杂度增加。

public class MyRun implements Runnable {

 

private int count = 5;

 

@Override

public void run() {

// 模拟买票

for (int i = 0; i < 5; i++) {

if (count > 0) {

count--;

System.out.println(Thread.currentThread().getName()+"卖出一张,还剩" + count + "");

}

}

}

}

 

package cn.sxt02.thread02;

 

public class Test02 {

public static void main(String[] args) {

 

MyRun run = new MyRun();

 

Thread t1 = new Thread(run,"窗口A");

Thread t2 = new Thread(run,"窗口B");

Thread t3 = new Thread(run,"窗口C");

 

t1.start();

t2.start();

t3.start();

}

}

 

 

比较ThreadRunnable区别

[1] 继承Thread的线程类不能再继承其他父类而实现Runnable接口还可以继承其他类。

[2] 实现Runnable接口的线程实现类,更便于多个线程共享资源。

 

 

 

 

1.4 线程的生命周期

1.4.1 生命周期

新生状态

new关键字建立一个线程后,该线程对象就处于新生状态。

处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。

就绪状态

处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU

当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为CPU调度”。

运行状态

在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任何而死亡。

如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。

阻塞状态

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己运行,进入阻塞状态。

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。

死亡状态

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过stop方法来终止一个线程【不推荐使用】;三是线程抛出未捕获的异常。

 

1.4.2 线程常见方法

1.4.3 [1]优先级

package cn.sxt03.thread03;

 

public class Test02Priority {

public static void main(String[] args) {

 

 

System.out.println(Thread.currentThread().getPriority());

 

// 线程优先级的最大最小默认值

System.out.println(Thread.MAX_PRIORITY);

System.out.println(Thread.MIN_PRIORITY);

System.out.println(Thread.NORM_PRIORITY);

 

Thread02 t2 = new Thread02("线程B");

t2.setPriority(Thread.NORM_PRIORITY);

 

Thread02 t1 = new Thread02("线程A");

t1.setPriority(Thread.MAX_PRIORITY);

 

t1.start();

t2.start();

}

}

线程优先级越大,表示被CPU调度的可能性增大并不一定执行。

 

1.4.4 [2]isAlive 检测线程是否处于活动状态。

package cn.sxt03.thread03;

 

public class Test03isAlive {

public static void main(String[] args) {

 

Thread t1 = new Thread("线程A");

System.out.println(t1.isAlive());

t1.start();

System.out.println(t1.isAlive());

 

}

}

 

 

1.4.5 [3]join 线程的强行执行

调用该方法的线程强制执行,其它线程处于阻塞状态,该线程执行完毕后,其它线程再执行

public class Test04Join {

public static void main(String[] args) {

 

Thread04 t1 = new Thread04("线程A");

t1.start();

 

for (int i = 0; i < 5; i++) {

if(i == 3) {

try {

t1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println("mainThread" + " i=" + i);

}

}

}

 

1.4.6 [4]sleep 线程休眠

当前线程休眠,线程进入阻塞状态。休眠时间到,进入就绪状态。在休眠过程中线程可以被中断。

 

 

1.4.7 [5]yield 线程礼让

public class Test06Yield {

public static void main(String[] args) {

 

 

Thread06 t1 = new Thread06("线程A");

t1.start();

 

 

for (int i = 0; i < 5; i++) {

if(i == 3) {

Thread.yield();

}

System.out.println("mainThread" + " i=" + i);

}

}

}

可能的结果

mainThread i=0

mainThread i=1

mainThread i=2 => 主线程礼让一次,下次有可能调度主线程,也有可能调度t1

mainThread i=3

mainThread i=4

线程A i=0

线程A i=1

线程A i=2

线程A i=3

线程A i=4

 

线程礼让后,线程进入就绪状态。

线程礼让让当前线程进入就绪状态(礼让一次)但调度者有可能再次调度礼让的线程。

 

1.4.8 [6]stop 线程终止

线程终止存在安全隐患,容易导致被锁的资源无法释放,不建议使用通常使用interrupt代替中断线程,中断线程并通过异常捕获,线程继续执行正常结束。

 

1.5 线程安全

多线程中允许多个线程访问同一个资源(共享资源),多个线程可以对共享资源进行破坏性操作容易导致数据错乱的问题。

 

原子性操作:逻辑上认为多句代码之间应该属于整体的,要么都执行,要么都不执行。不允许存在执行一半的情况。这样的操作称为原子性操作。

1.5.1 同步代码块

如果是少量的代码,可以把原子性操作放入同步代码块中,使用关键字synchronized语法

synchronized ( 同步监视器/互斥锁 ){

   原子性操作

}

同步监视器/互斥锁一定对象类型。

 

package cn.sxt04.thread04;

 

public class MyRun implements Runnable {

 

private int count = 5;

 

@Override

public void run() {

// 模拟买票

for (int i = 0; i < 5; i++) {

// mutex 互斥锁

// 通常将当前对象作为同步对象/互斥锁

synchronized (this) {

if (count > 0) {

count--;

 

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

 

System.out.println(Thread.currentThread().getName() + "卖出一张,还剩" + count + "");

}

}

}

}

}

 

 

同步代码块本质上是给共享资源加锁。会导致其他访问共享资源的线程阻塞。

1.5.2 同步方法

如果代码量多,可以考虑使用同步方法。

package cn.sxt04.thread04;

 

public class MyRun implements Runnable {

 

private int count = 5;

 

@Override

public void run() {

// 模拟排队买票

for (int i = 0; i < 5; i++) {

this.buyTicket();

}

}

 

// 同步方法默认把当前对象this加锁

public synchronized void buyTicket() {

if (count > 0) {

count--;

 

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

 

System.out.println(Thread.currentThread().getName() + "卖出一张,还剩" + count + "");

}

}

}

 

 

同步监视器

  • synchronized(obj){}中的obj称为同步监视器
  • 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身

 

同步监视器的执行过程

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器未锁,锁定并访问

 

1.5.3 死锁(C

线程A拥有一个资源r1,再去申请另外一个资源r2时;此时线程B拥有r2资源,再去申请r1此时两个线程陷入互相等待陷入阻塞,此时就绪队列为空,cpu空转。

 

package cn.sxt05.thread07;

 

public class ThreadA extends Thread{

private Object r1;

private Object r2;

 

public ThreadA(Object r1, Object r2) {

super();

this.r1 = r1;

this.r2 = r2;

}

 

@Override

public void run() {

synchronized (r1) {

System.out.println("已拥有r1");

 

synchronized (r2) {

System.out.println("想申请拥有r2");

}

}

}

}

 

 

 

1.6 线程间通信

 

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄