本周主题:Java多线程技术

目录

​一、Java中的多线程概念​

​1、线程的生命周期​

​2、线程的优先级​

​3、线程的几个主要概念​

​二、多线程的使用​

​1、线程创建方法​

​2、线程的优先级​

​3、线程的同步机制​

​三、Java多线程售票小实例​

​1、错误示范​

​2、正确示范​

​3、总结:​


一、Java中的多线程概念


Java 为多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是多任务的一种特别的形式,多线程使用了更小的资源开销。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

这里定义和线程相关的另一个术语 - 进程:也就是一个应用程序。一个进程包括由操作系统分配的内存空间,包含一个或多个线程(至少要有一个线程,叫做主线程)。一个线程不能独立的存在,它必须是进程的一部分。一个进程(程序)会一直运行,直到所有的非守护线程都结束运行后才能结束。

Java的线程是通过java.lang.Thread类来实现的。JVM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。


1、线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。在Java当中,线程通常都有五种状态:创建、就绪、运行、阻塞和死亡。

下图显示了一个线程完整的生命周期。

Java第十五周作业_java

  • 新建状态:使用new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

2、线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1-10(Thread.MIN_PRIORITY - Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 5(NORM_PRIORITY)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。


3、线程的几个主要概念

在多线程编程时,你需要了解以下几个概念:

  • 线程同步
  • 线程间通信
  • 线程死锁
  • 线程控制:挂起、停止和恢复

二、多线程的使用

有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。

请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!


1、线程创建方法

线程的创建有两种方式,第一种是继承Thread类,第二种是实现Runnable接口。

多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start()就是进入随时可以开始玩的状态!当CPU分配一个时间片段给你就是轮到你了,你就开始玩run(),当CPU的分配给你的时间片使用完毕,你(线程)就继续排队,等待下一次的run()

调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可运行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。

1、start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

2、run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

注意:多线程就是分时利用CPU,在宏观上就是让所有线程一起执行 ,也叫并发

  • 采用实现 Runnable接口的方式创建多线程时,线程类只是实现了 Runnable 接口,还可以继承其他类。
  • 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
  • 1、写一个类继承自Thread类,重写run方法。用start方法启动线程:
/**
* 文件名:MyFirstThreadTest.java
* 功能描述:继承Thread类,重写run()方法实现多线程功能的测试Demo
*/
class MyThread extends Thread {
public MyThread() {
super();
}

public MyThread(String threadName) {
super(threadName);
}

@Override
public void run() {
super.run();
for(int i = 5;i > 0; i--) {
try {
Thread.sleep(100); //当前线程休眠100ms
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + (i));//输出线程名
}
}
}

public class MyThreadTest {
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
try {
for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName()+"-->"+(i));
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行结果:

 

Java第十五周作业_多线程_02


  • 2、写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动:
/**
* 文件名:RunnableTest.java
* 功能描述:使用Runnable接口实现Thread功能的测试Demo
*/
class RunThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.currentThread().sleep(100);//Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"-->"+i);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class RunnableTest {
public static void main(String[] args) {
Runnable rt1 = new RunThread();
Runnable rt2 = new RunThread();

Thread t1 = new Thread(rt1,"线程1");
Thread t2 = new Thread(rt2,"线程2");

t1.start();
t2.start();
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}

运行结果:

  

Java第十五周作业_java_03


2、线程的优先级

setPriority()和join()方法的使用举例:

/**
* 文件名:MyPriorityTest.java
* 功能描述:多线程优先级功能的测试Demo
*/
class MyThread extends Thread {
public MyThread() {
super();
}

public MyThread(String threadName) {
super(threadName);
}

@Override
public void run() {
super.run();
for (int i = 5; i > 0; i--) {
try {
Thread.sleep(100); //当前线程休眠100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + (i));//输出线程名
}
}
}

public class MyPriorityTest {
public static void main(String[] args) {
Thread t1 = new MyThread("t1");
try {
t1.start();
//可以比较一下有无t1.join();语句的执行效果
t1.join(); //等待本线程执行完毕后,才会去执行其他线程,相当提升了t1对象的优先级为最高级(10)

for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName() + "-->" + (i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

有无t1.join();语句的执行结果比较: 

  

Java第十五周作业_多线程_04

Java第十五周作业_多线程_05


3、线程的同步机制

1)为什么要使用synchronized

在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

2)实现原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

3)synchronized的三种应用方式

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
  • 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  • 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

4)synchronized的作用

Synchronized是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码


5、synchronized用法举例:

/**
* 文件名:MySynchronizedTest.java
* 功能描述:线程同步关键字synchronized的用法测试Demo
*/
public class MySynchronizedTest {
private int shareInt = 0;
class AddThread extends Thread {
@Override
public void run() {
super.run();
synchronized (MySynchronizedTest.this) { //对以下代码块加锁,代码块执行完毕,释放该锁
System.out.println("当前线程:"+Thread.currentThread().getName());//输出线程名
try {
System.out.println("自增之前,shareInt="+shareInt);
Thread.sleep(100); //当前线程休眠100ms
shareInt ++;
System.out.println("自增之后,shareInt="+shareInt);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class SubThread extends Thread {
@Override
public void run() {
super.run();
synchronized (MySynchronizedTest.this) { //对以下代码块加锁,代码块执行完毕,释放该锁
System.out.println("当前线程:"+Thread.currentThread().getName());//输出线程名
try {
System.out.println("自减之前,shareInt="+shareInt);
Thread.sleep(100); //当前线程休眠100ms
shareInt --;
System.out.println("自减之后,shareInt="+shareInt);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public MySynchronizedTest() {
Thread t1 = new AddThread();
Thread t2 = new SubThread();
t1.start();
t2.start();
/*
for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName() + "-->" + (i));
}
*/
}

public static void main(String[] args) throws InterruptedException {
MySynchronizedTest test = new MySynchronizedTest();
}
}

 使用synchronized进行加锁前后执行结果对比:

  

Java第十五周作业_java_06

Java第十五周作业_多线程_07


三、Java多线程售票小实例

1、错误示范

/**
* 文件名:SellThreadTest.java
* 功能描述:多线程售票测试Demo
*/
class SellThread implements Runnable {
private int i = 5; //票的数量

public void run() {
while (true) {
if (i > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
} else {
break;
}
}
}
}

/**
* 文件名:Test.java
* 功能描述:
*/
public class SellThreadTest {
public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}

没有同步控制变量,所以最后出现了一票多卖和-1张票的情况:

 

Java第十五周作业_java_08

2、正确示范

class SellThread implements Runnable {
private int i = 5;
String key = " ";

public void run() {
while(true) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (key) {
if (i > 0) {
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
} else {
break;
}
}
}
}
}

public class SellThreadTest {

public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}

执行结果如下:

Java第十五周作业_优先级_09

补充:如果用synchronized关键字修饰方法,则只能有一个线程获取访问这段方法的权限。也就是说,一个线程把票买完了才结束。

class SellThread implements Runnable {
private int i = 20;
String key = "";

public synchronized void run() {
while(true) {
if(i > 0) {
try {
Thread.sleep(100);
} catch(Exception e) {

}
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
}else {
break;
}
}
}
}

public class SellThreadTest {

public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}

执行结果:一个人把所有的票都卖完了!

  

Java第十五周作业_多线程_10

3、总结:

1、synchronized()括号中需要的是一个对象,所以这里使用了String类的一个对象key,可以用this来代替key。

2、一个线程拿到synchronized括号中的对象以后,其他线程也是可以执行的,但是当它们需要用key的时候,发现key已经被别人拿走了,只能等着key被释放了。就像上厕所,坑被别人占了,只能等着了,但是等着的时候我还可以运行,比如玩玩手机什么的。

3、synchronized method(){} 可以防止多个线程同时访问这个对象的synchronized方法;如果一个对象A,有多个synchronized方法,只要一个线程访问了其中的一个,那么其他线程不能同时访问A的任何一个synchronized方法。但是可以访问对象B的synchronized方法。

4、synchronized static method(){} 可以防止多个线程同时访问这个类的synchronized方法;对类的所有对象均起作用,无论对象A、对象B、对象C的。