[学习笔记]Java多线程

时间:2023-02-25 18:22:14

概述

1. 概念

进程:程序在一个数据集合上运行的过程,是系统进行资源分配的独立单位。 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 简而言之,进程是系统进行资源分配的最小单位,而线程是系统进行调度的最小单位。

2. 主线程

JVM启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程并发执行的方式,就叫做多线程。

3. 多线程的目的


  • 当主线程在这个程序中执行时,如果遇到了循环而导致在指定为停留时间过长,无法执行下面的程序容易造成程序假死现象。这时可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行。从而实现多部分代码并发执行来避免假死。
  • 为了充分利用多处理机的执行效率,可以使用多线程并行(并发)执行。

CPU调度机制

现代CPU一般采取基于时间片轮转的调度算法,大致思路为系统将所有就绪线程按先来先服务原则组成队列,每次调度将CPU分配给队首线程,并执行一个时间片,时间片大小固定且一般时间较短(微秒级),当执行时间片用完发生中断,将线程停止并送到就绪队列的末尾重新进行排队,然后将CPU分配给新的队首线程,以此类推。这样就可以保证就绪队列中的所有线程在一定的时间内均能获得时间片进行处理的时间,换言之,系统能在给定时间响应所有线程的请求。现代用的调度算法多是在此基础上的多级反馈队列调度算法,所以对程序员来说,某一时刻哪个线程在执行是未知的,调度也是近似随机的。

线程的创建

方法一:继承Thread类

1. 定义一个线程类继承Thread。 2. 重写run方法。将线程任务代码定义到run方法中。 3. 创建子类对象,就是创建线程对象。 4. 调用start方法,开启线程并让线程执行,同时还会告诉JVM去调用run方法。     

注意

  • 主线程的任务定义在main方法中。
  • Java中的线程分为两部分:一部分线程对象(Thread对象),一部分线程任务(run()方法)。
  • 自定义线程的任务都定义在run方法中。
  • Thread类中的run方法内部的任务并不是我们所需要,只需要重写这个run方法即可。
  • Thread类的实现方式类似模板方法,主要算法在start()方法中,而run()方法需要在子类中明确,虽然run()方法在Thread类中不是抽象的,但是直接建立Thread类对象然后调用start()方法是无意义的,因为此时run()方法没有重写,所以没有内容。

Thread类源码

class Thread {  private Runnable target;  Thread(Runnable target) { this.target = target; }  public void run() { if (target != null) { target.run(); } }  public void start() { run(); }}

示例

class PrintName extends Thread { String name = null;  PrintName(String name) { this.name = name; }  public void run() { for (int i = 0; i < 20; i++) { System.out.println( "name: " + this .name ); } }} public class ThreadDemo { public static void main(String[] args) { PrintName n1 = new PrintName( "张三"); PrintName n2 = new PrintName( "李四"); n1.start(); // 创建一个线程打印“张三” n2.run(); // 利用主线程打印“李四" }}

运行结果

可以看到“张三”和“李四”的名字交替打印。

方法二:实现Runnable接口(常用)

1. 实现Runnable接口:避免了继承Thread类的单继承局限性。 2. 重写接口中的run方法:将线程任务代码定义到run方法中。 3. 创建Thread类的对象:只有创建Thread类的对象才可以创建线程,因为具体算法实现在Thread类的start()方法中。 4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。 5. 调用Thread类的start方法开启线程。

注意

  • 实现Runnable接口方式避免了单继承的局限性,所以较为常用。
  • 实现Runnable接口的方式,更加的符合面向对象。
  • Runnable接口对线程对象和线程任务进行解耦。

实例

/** * 卖票程序,4个窗口同时卖同一种票,记录剩余票数量 */class TicketSale implements Runnable { // 线程共享资源 private int tickets = 100;  public void run() { // 重写Runnable的run()方法 while (tickets > 0) // 利用双重检查加锁方法 { try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println(Thread. currentThread().getName() + " Sell :" + tickets --); } }} public class ThreadDemo { public static void main(String[] args) { TicketSale t = new TicketSale(); // 创建4个线程同时卖票 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); }}

运行结果

可以看到4个线程同时交替卖票,但是最终结束可能会卖出第0号票,第-1号票等,这是由于多线程访问共享资源时出现的错误,问题的解决牵涉到线程安全问题。

两种线程创建方式的区别

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

线程状态

就绪:允许进入执行状态,只要获得了CPU调度时间片就可执行。 执行:线程正在运行。 阻塞:线程需要等待获取某个资源,或者其他原因需要等待,则阻塞。 [学习笔记]Java多线程

线程的内存结构

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈。当执行线程的任务结束了,线程自动在栈内存中释放了。当所有的执行线程都结束了,那么进程就结束了。

获取线程

Thread.currentThread()获取当前线程对象。 Thread.currentThread().getName()获取当前线程对象名称;

线程名称

主线程:main 自定义的线程:Thread-1,线程多个时,数字顺延。Thread-2……

线程安全

原因

  • 线程任务中在操作共享的数据。
  • 线程任务操作共享数据的代码有多条(运算有多个),容易造成共享数据的混乱。

解决思路

只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,在执行过程中,不要让其他线程参与运算。

格式

方法一:使用关键字synchronized进行同步

格式一

synchronized (对象锁) {      //需要被同步的代码。 }

格式二

使用 synchronized 修饰函数。

示例

/** * 卖票程序,两个窗口同时卖同一种票,记录剩余票数量 * 使用synchronized方式同步 */class TicketSale implements Runnable { // 共享资源 private int tickets = 100; // 定义flag标志是两个卖票窗口,true为1号窗口,false为2号窗口 boolean flag = true;  public void run() { if (flag) { // 1号窗口开始卖票 while (tickets > 0) { // 使用同步代码块(锁必须一致,由于同步函数锁为this,所以这里必须是this) Wait(10); synchronized (this ) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + " Sell 1 :" + tickets--); } } } } else { // 2号窗口开始卖票 while (tickets > 0) { Wait(10); // 使用同步函数(默认锁为this) func(); } } }  synchronized void func() { if (tickets > 0) { System.out.println(Thread. currentThread().getName() + " Sell 2 :" + tickets--); } }  static void Wait( int time) { try { Thread.sleep(time ); } catch (InterruptedException e) { } }} public class ThreadSafe { public static void main(String[] args) {  TicketSale t = new TicketSale(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); TicketSale.Wait(10); t.flag = false; t2.start(); }}
运行结果
窗口1和窗口2同时卖票,并且不再对共享资源同时访问,所以没有卖出第0号票等错误。

同步优势与弊端

  • 解决多线程安全问题。
  • 降低了程序的性能。如果同步中嵌套了其他的同步,容易产生死锁。

同步前提

  • 必须保证在同步中有多个线程。
  • 必须保证多个线程在同步中使用的是同一个锁。

同步函数和同步代码块的区别

  • 同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
  • 同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。同步代码块较为常用。
  • static同步函数,使用的锁不是this,而是字节码文件对象:类名.class

方法二:使用Lock锁机制(Java 5支持,建议使用)

import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.Condition;

Lock接口

比同步具有更多功能:
  • 获取锁
    lock();
  • 释放锁
    unlock();
  • 获取该锁的监视器对象,可以使用该对象做更精确地线程通讯。
    newCondition();

注意

  • 一般情况下,Lock对象,Condition对象均不可改变,即使用private final关键字修饰。
  • 程序中可以使用多个锁(Lock)用于不同资源的同步,同一个锁(Lock)也可以建立多个信号量(Condition)用于多种目标的线程通讯。
  • 获取锁lock()后必须要释放锁unlock(),不然会引起线程死锁,所以应使用try{}fianlly{}结构,而unlock()应放在finally中。

实例

import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * 卖票程序,两个窗口同时卖同一种票,记录剩余票数量 * 使用Lock方式同步 */class TicketSale implements Runnable { // 多个对象共享资源 private static int tickets = 100; // 定义买票窗口编号 private int num = 0;  public TicketSale( int num) { super(); this.num = num; }  // 定义锁,多个对象使用同一个锁 private final static Lock lock = new ReentrantLock();  public void run() { while (tickets > 0) { Wait(10); lock.lock(); try { func(num); } finally { // 释放锁是必须执行的 lock.unlock(); } } }  private void func( int num) { if (tickets > 0) { System.out.println(Thread. currentThread().getName() + " Sell " + num + " : " + tickets--); } }  static void Wait( int time) { try { Thread.sleep(time); } catch (InterruptedException e) {} }}  public class ThreadSafe { public static void main(String[] args) {  new Thread( new TicketSale(0)).start(); new Thread( new TicketSale(1)).start(); new Thread( new TicketSale(2)).start(); new Thread( new TicketSale(3)).start();  }}
运行结果
4个窗口同时售票,并且有条不紊。

死锁

当使用嵌套同步(锁)时,可能发生多个线程同时拥有了资源的一部分,等待另一部分资源的情况,这时候所有的线程均持有资源,却又不愿释放该资源,导致其他线程同样在等待资源,必然产生没有一个线程可以继续执行下去,而且永远等不到获取对方释放的资源,这就产生死锁。

示例

class Locks { static final Object LockA = new Object(); static final Object LockB = new Object();}  class Demo implements Runnable { private boolean flag = true;  Demo(boolean flag) { this.flag = flag; }  public void run() { if (flag) { synchronized (Locks. LockA) { System. out.println("1. Get LockA!" ); synchronized (Locks.LockB) { System. out.println("1. Get LockB!" ); } } } else { synchronized (Locks. LockB) { System.out.println("2. Get LockB!" ); synchronized (Locks.LockA) { System.out.println("2. Get LockA!" ); } } } }} public class ThreadDeadLock { public static void main(String[] args) { System.out.println("Hello World!"); Demo d1 = new Demo(true); Demo d2 = new Demo(false); new Thread(d1).start(); new Thread(d2).start(); }}

运行结果

Hello World! 1. Get LockA! 2. Get LockB! 程序仍然在运行,死锁。

线程通讯

线程可以主动放弃执行权,也可以唤醒其他阻塞的线程,这些行为叫做线程通讯。

方法一:使用Object类的方法

wait(); // 会让线程处于阻塞状态,其实就是将线程临时存储到了线程池中。 notify(); // 会唤醒线程池中任意一个等待的线程。 notifyAll(); // 会唤醒线程池中所有的等待线程。

注意

  • notify();唤醒线程池中任意的线程,所以该方法可能导致没有唤醒该唤醒的线程导致程序仍然无法继续下去。
  • notifyAll();唤醒线程池中所有的等待线程,所以可能唤醒了不该唤醒的线程占用CPU的调度时间,导致性能低下。
  • 这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁(信号量)。同一个锁上的notify,只能唤醒该锁上的被wait的线程。

实例

/** * 仅存一个产品的生产者消费者问题 * 有一群生产者在生产产品,并将这些产品提供给消费者去消费。为使生产者与消费者能够并发执行,在两者之间设置一个具有 n * 个缓冲区的缓冲池,生产者将他所生产的产品放入一个缓冲区中;消费者可从一个缓冲区中取走产品去消费。尽管所有的生产者和消费者都是以异步方式运行, * 但他们之间必须保持同步 ,即不允许消费者到一个空缓冲区去取产品;也不允许生产者向一个已装满产品且尚未被取走的缓冲区投放产品。 * 这里先仅讨论 n = 1 的情况,缓冲池用一个 flag 标志代替。 * * 当生产者和消费者只有一个时: * 1. 产品为临界资源,增加同步锁,避免出现消费的是早前生产的已经丢弃的产品。 * 2. 使用 wait 和 notify 方法进行线程通讯,避免出现连续多次生产和多次消费。 * * 当生产者和消费者不止一个时: * 3. 当一种角色全都等待时,另一种角色不同者容易发生循环等待 -唤醒,导致死锁,这时使用 notifyAll 方法代替 notify * 方法释放全部等待的线程,避免死锁情况发生。 */ class Resource { private int num = 0; // 生产的产品编号,生产一个就加1,并打印。 boolean flag = false; // flag代表仓库,false代表无产品,true代表有产品。 public static final int MAX = 100;  public boolean isMax() { return num >= MAX; }  public synchronized void put() // 生产函数 { while ( flag) // 如果有产品,则等待 { System.out.println(Thread. currentThread().getName() + " wait!"); try { wait(); } catch (InterruptedException e) {} // 暂时不做异常处理 // 唤醒后需要重新判断是否有产品,所以使用while } System.out.println(Thread. currentThread().getName() + "......生产..." + ++num ); flag = true; // 使用notifyAll()方法避免同一种角色循环等待-唤醒照成死锁 notifyAll(); }  public synchronized void get() // 消费函数 { // 同样地,可以用if-else代替while循环,避免未经判断得得到资源。 if (!flag) { System.out.println(Thread. currentThread().getName() + " wait!"); try { wait(); } catch (InterruptedException e) {} } else { System.out.println(Thread. currentThread().getName() + "......消费..." + num ); flag = false; notifyAll(); } }} class Producer implements Runnable { private Resource r;  public Producer(Resource r) // 将公共的资源(仓库)传入构造函数 { this.r = r; }  public void run() { while (! r.isMax()) { r.put(); // 循环生产100次 } }} class Consumer implements Runnable { private Resource r;  public Consumer(Resource r) { this.r = r; }  public void run() { // 循环消费100次,并消费掉所有库存 while (! r.isMax() || r. flag) { r.get(); } }} public class Thread_ProducerConsumer { public static void main(String[] args) { Resource r = new Resource(); Producer p = new Producer(r); Consumer c = new Consumer(r); new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); }}

运行结果

生产者生产后,其他生产者就等待,直到消费者将其消费为止。同理,没有库存时消费者同样等待生产者的生产。 Thread-0......生产...1 Thread-0 wait! Thread-3......消费...1 Thread-3 wait! Thread-2 wait! Thread-1......生产...2 Thread-1 wait! Thread-2......消费...2 Thread-2 wait! Thread-3 wait! Thread-0......生产...3 Thread-0 wait! Thread-3......消费...3 Thread-3 wait! ……

方法二:使用Condition类的方法(Java 5支持,建议使用)

监视器方法封装在 Condition 对象中,需要配合Lock接口使用,想要获取监视器方法,需要先获取 Condition 对象。
Condition 对象: await(); // 等待 signal(); // 唤醒 signalAll(); // 唤醒所有

注意

当需要唤醒属于特定功能的线程时建议使用Condition类方法。
  • 如果使用synchronized则可以通过两个锁嵌套完成,因为Object的线程通讯方法在同一个锁内只能针对同一个信号量。
  • 如果使用Lock + Condition类方法,则可以在一个锁上定义多个信号量(Condition监视器对象),这样就可以完成唤醒特定线程的功能。

实例

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /* * 仅存一个产品的生产者消费者问题 * 使用Lock + Condition类方法实现 */ class Resource { private int num = 0; // 生产的产品编号,生产一个就加1,并打印。 boolean flag = false; // flag代表仓库,false代表无产品,true代表有产品。 public static final int MAX = 100;  private final Lock lock = new ReentrantLock(); private final Condition isEmpty = lock.newCondition(); private final Condition isFull = lock.newCondition();  public boolean isMax() { return num >= MAX; }  public void put() // 生产函数 { lock.lock(); try { while ( flag) // 如果有产品,则让生产者等待 { System.out.println(Thread.currentThread().getName() + " wait!"); try { // 让生产者等待 isEmpty.await(); } catch (InterruptedException e) {} // 暂时不做异常处理 // 唤醒后需要重新判断是否有产品,所以使用while } System.out.println(Thread. currentThread().getName() + "......生产..." + ++num ); flag = true; // 唤醒消费者 isFull.signal(); } finally { lock.unlock(); } }  public void get() // 消费函数 { lock.lock(); try { while (! flag) { System.out.println(Thread.currentThread().getName() + " wait!"); try { isFull.await(); } catch (InterruptedException e) {} } System.out.println(Thread. currentThread().getName() + "......消费..." + num ); flag = false; isEmpty.signal();  } finally { lock.unlock(); } }} class Producer implements Runnable { private Resource r;  public Producer(Resource r) // 将公共的资源(仓库)传入构造函数 { this.r = r; }  public void run() { while (! r.isMax()) { r.put(); // 循环生产100次 } }} class Consumer implements Runnable { private Resource r;  public Consumer(Resource r) { this.r = r; }  public void run() { // 循环消费100次,并消费掉所有库存 while (! r.isMax() || r. flag) { r.get(); } }} public class Thread_ProducerConsumer { public static void main(String[] args) { Resource r = new Resource(); Producer p = new Producer( r); Consumer c = new Consumer( r); new Thread( p).start(); new Thread( p).start(); new Thread( c).start(); new Thread( c).start(); }}

其他

1. 线程的匿名内部类

class anonyThread { public static void main(String[] args) { new Thread( new Runnable() { public void run() { System. out.println("runnable run" ); } }) { public void run() { System. out.println("subthread run" );// 执行。 } }.start(); }}

2. sleep方法和wait方法异同点

相同点:可以让线程处于阻塞状态。 不同点:
  • sleep()必须指定时间。
    wait()可以指定时间,也可以不指定时间。
  • sleep()时间到,线程处于临时阻塞或者运行。
    wait()如果没有时间,必须要通过notify()或者notifyAll()唤醒。
  • sleep()不一定非要定义在同步中。
    wait()必须定义在同步中。
  • 若都定义在同步中:
    线程执行到sleep(),不会释放锁。
    线程执行到wait(),会释放锁。

3. 线程的终止

  • 停止线程的方法stop已经过时。
  • run方法结束即可,只要通过标记或者循环变量控制住循环就可以了。
  • 如果线程处于阻塞状态,就无法判断标记,就无法结束。这时就需要通过结束线程的阻塞状态让其恢复到运行状态。
    可以通过Thread类中的interrupt方法解决这个问题。
    注意:该方法会使线程的wait()、await()出现InterruptedException异常,要做出处理方式,比如更改标记或循环变量使之自然结束。

4. 守护线程(后台线程)

只要线程在调用 start() 方法前调用了 setDaemon(true) 方法,就可以把线程标记为守护线程。 当进程中所有的前台线程都结束了,这时无论后台线程处于什么样的状态,都会结束,从而进程会结束。进程结束依赖的都是前台线程。

5. 线程的优先级

线程优先级用数字标示:1-10,1最小,10最大。 一般使用setPriority(Thread.MAX_PRIORITY); MIN_PRIORITY:1 NORM_PRIORITY:5 MAX_PRIORITY:10

6. 线程组(ThreadGroup)

可以通过Thread的构造函数明确新线程对象所属的线程组。 线程组可以对多个同组线程,进行统一的操作。 默认都属于main线程组。

7. Join方法和yield方法

Join()方法:加入一个执行线程,当前线程释放执行权,直到加入的线程执行完毕为止。 yield()方法:暂停线程,释放执行权。

8. setDaemon(boolean b)

将线程设置为守护线程,程序中所有的非守护线程结束,进程就结束。

9.setName(String name)

设置线程名称

10. 示例

import java.util.Random; class Demo implements Runnable {  public void run() { for (int x = 1; x <= 10; x++) { // 随机sleep一段时间 Random random = new Random(); int i = Math.abs(random .nextInt() % 100 + 1); try { Thread. sleep((long) i); } catch (InterruptedException e) { System. out.println("haha" ); e.printStackTrace(); } // 输出语句 System.out.println(Thread. currentThread().getName() + "..." + x ); Thread.yield(); // 线程临时暂停。将执行权释放,让其他线程有机会获取执行权。 } }} public class ThreadOthers { public static void main(String[] args) { // 线程的匿名内部类 new Thread( new Runnable() { public void run() { System. out.println("runnable run" ); } }) { public void run() { System. out.println("subthread run" ); // 执行。 } }.start();  Demo d = new Demo(); Thread t1 = new Thread( d); Thread t2 = new Thread( d);  t1.start(); t2.start();  // 临时加入一个运算的线程。让该线程运算完,程序才会继续执行。 try { t1.join(); } catch (InterruptedException e) {} // 主线程执行到这里,直到t1要加入执行,主线程释放了执行权,执行资格并处于阻塞状态,直到t1线程执行完毕。  for (int x = 1; x <= 10; x++) { System.out.println( "main..." + x ); } System.out.println( "over"); }}

运行结果

subthread run Thread-1...1 Thread-1...2 Thread-2...1 Thread-2...2 Thread-1...3 Thread-1...4 Thread-2...3 Thread-1...5 Thread-2...4 Thread-1...6 Thread-1...7 Thread-2...5 Thread-2...6 Thread-1...8 Thread-2...7 Thread-1...9 Thread-2...8 Thread-1...10 main...1 main...2 main...3 main...4 main...5 main...6 main...7 main...8 main...9 main...10 over Thread-2...9 Thread-2...10