1、线程的概念
? 多线程,就类似与操作系统中的多进程。简单的讲,就是可 以同时并发执行多个任务,处理多件事情。这与我们经常所 谓的边唱边跳,边说边做事一个道理。
? 线程是一个轻量级的进程,一个进程中可以分为多个线程。 比起进程,线程所耗费的系统资源更少,切换更加容易
/* * 进程是操作系统中的一个任务,一个程序启动运行,就会创建 * 一个(或多个)进程。 * 线程是轻量级的进程。进程会有自己独立的内存空间与资源。一个进程 * 下会存在一个(或多个)线程。线程为进程的执行单元。线程本身不含有 * 独立的资源,而是共享进程中的资源。 * * 多线程的优势: * 1 使用多线程可以提高CPU的利用率。 * 2 使用多线程可以提供更好的动态性与交互性。 */ package day18; public class HelloWorld { public static void main(String[] args) { // 无限循环,用来查看java程序的进程。 // Java程序在运行时,就会存在一个进程。 // 一个进程至少要包含一个线程,Java线程就是 // 执行main方法的main线程(线程的名字叫做main)。 while (true) ; } }
2、Thread类
? Thread是Java中的线程类,我们可以通过继承Thread类来实 现多线程操作。
? 继承Thread类,重写run方法, run方法中的代码即为我们需 要线程执行的任务,然后调用对象的start方法即可启动线程。
说明:
? 不是调用run方法,而是调用start方法,否则无法实现多线 程。
? 执行main方法就会创建一个线程。
/* * Thread类就表示线程。可以用来创建线程。 * 通过继承Thread类实现多线程。 */ package day18; public class ThreadTest { public static void main(String[] args) { // 创建线程 Mission1 m = new Mission1(); Mission2 m2 = new Mission2(); // 创建线程后,需要启动线程,线程才能够执行。 // 调用start方法来启动一个线程,是线程处于就绪 // 状态。线程处于就绪状态,不代表该线程会马上得到执行, // 具体何时执行,要取决于操作系统的调度。线程处于就绪 // 状态,表示该线程有机会获得CPU的时间片,即有机会 // 得到执行。 m.start(); m2.start(); } } // run方法就是线程要执行的任务。 // 我们重写Thread类的run方法。 // 线程从run方法开始执行。 class Mission1 extends Thread { @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(i); // 令当前线程休眠参数指定的时间(毫秒) // 处于休眠的线程会放弃掉当前的时间片, // 并且在整个休眠期间,不会获得CPU的时间片。 // 当线程苏醒时,线程会处于就绪状态。(不是运行状态)。 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 获取线程的名字。 System.out.println(getName()); } } } class Mission2 extends Thread { @Override public void run() { for (int i = 11; i <= 20; i++) { System.out.println(i); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }
3、Runnable接口
除了继承Thread类以外,还可以通过实现Runnable接口来实现 多线程。
? 首先,我们要创建一个类,该类实现Runnable接口, Runnable接口中存在一个抽象方法run,因此,我们需要实 现(重写) run方法。
? 然后创建该类的对象,将对象作为Thread的运行目标。 ( target)。
/* * 通过实现Runnable接口实现多线程 */ package day18; public class RunnableTest { public static void main(String[] args) { Mission m = new Mission(); // 使用实现Runnable接口的对象,作为 // 线程的目标执行体。 Thread t1 = new Thread(m); Thread t2 = new Thread(m); t1.start(); t2.start(); } } class Mission implements Runnable { @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(i); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } // 无法直接调用getName(),因为当前类没有继承Thread, // 不是Thread类的子类。 // getName(); // 获取当前的执行的线程。 Thread current = Thread.currentThread(); current.getName(); } }
4、线程的生命周期
线程的生命周期可以分为以下环节:
? 新建-创建对象
? 就绪-调用start后
? 运行-获得CPU资源
? 阻塞(挂起) -失去CPU资源
? 死亡-线程执行结束或抛出未捕获的异常
5、常用方法
public static Thread currentThread(); 获得当前执行的线程。 ? public String getName() 获得线程名称。 ? public final native boolean isAlive(); 判断线程是否存活 (在start方法调用后,并且线程没有死亡)。 ? public void join() 等待直到这个线程死亡。 ? public static void sleep(long millis) 使当前线程睡眠(暂时 停止执行) millis毫秒。如果当前程序存在其他等待的线程, 则其他线程会获得执行机会。? public static void yield();当前运行的线程有意让出CPU资源,由线程调度器重新选择线程调度。不过,这仅仅是一个提示而已,线程调度器可能会忽略。? public void setDaemon(boolean on);设置线程是否为后台线程。? public void setPriority(int newPriority)设置线程的优先级。优先级为1-10。优先级高的线程仅意味着可能获得更多执行的机会,不表示一定会一直执行,优先级低的线程仍然有机会执行。
6、yield方法
package day18; public class YieldTest { public static void main(String[] args) { Thread2 t = new Thread2(); Thread3 t2 = new Thread3(); t.start(); t2.start(); } } class Thread2 extends Thread { @Override public void run() { for (int i = 0; i <= 5; i++) { System.out.println(i); } // 当前线程有意让出CPU资源,让其他线程得到执行。 // 但是,操作系统可能会忽略线程的这种请求。 Thread.yield(); for (int i = 6; i <= 9; i++) { System.out.println(i); } } } class Thread3 extends Thread { @Override public void run() { for (int i = 100; i < 110; i++) { System.out.println(i); } } }
7、
package day18; //执行main方法的线程名就叫main。 public class ThreadMethod { public static void main(String[] args) { // 获取当前执行的线程。 Thread current = Thread.currentThread(); // 获取线程的名字。 // System.out.println(current.getName()); // 判断当前线程是否处于存活状态(调用start方法之后, // 线程死亡之前),存活返回true,否则返回false。 // System.out.println(current.isAlive()); ThreadA ta = new ThreadA(); ta.start(); try { // 如果在A线程中调用B线程对象的join方法(无参),则A线程 // 会一直等待B线程执行结束,A线程才会继续执行。 ta.join(); // 当前线程最多等待ta线程参数指定的时间。(毫秒) // ta.join(500); // 当前线程最多等待第一个参数的时间(毫秒)加上 // 第二个参数的时间(纳秒)。(时间的和) // ta.join(500, 100); // 令当前线程休眠参数指定的时间。 // Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main方法输出内容"); ThreadA ta2 = new ThreadA(); // 设置当前线程是否为后台线程,true为后台线程,否则 // 为前台线程。该方法需要在线程启动之前调用。 ta2.setDaemon(true); // 设置线程的优先级。优先级高的线程不代表会一直执行, // 只是执行几率大于优先级低的线程。虽然Java提供了 // 1-10这十个优先级,但是,操作系统未必会存在十个优先级 // 有Java相对应。 ta2.setPriority(Thread.NORM_PRIORITY); } } class ThreadA extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
8、中断线程
package day18; public class InterruptTest { public static void main(String[] args) { Thread4 t = new Thread4(); t.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 中断线程。 t.interrupt(); } } class Thread4 extends Thread { @Override public void run() { try { // 如果线程处于sleep,join等等待方法中,如果 // 在等待期间,线程被其他线程所中断,则会产生 // InterruptedException异常。如果没有处于 // 以上等待方法中,则线程被其他线程所终端,不会 // 产生InterruptedException异常。 Thread.sleep(10000); } catch (InterruptedException e) { System.out.println("产生了InterruptedException异常"); } } }
9、线程同步
当多线程并发运行时,多线程间很可能操作共享成员变量,此 时,就需要对共享成员变量的操作进行同步,避免出现多线程 的并发修改而引起的意外错误。
当多线程并发修改公共变量时,我们就需要对公共变量的修改 部分进行同步,避免出现并发修改错误。
线程同步可以使用:
? 同步块
? 同步方法
同步的代码在同一时刻,至多只会有一个线程执行。其使用线 程锁机制来保证。
/* * 当多线程对共享变量并发进行修改时,就可能会出现 * 并发修改的不一致性。 * 对共享变量进行并发修改的区域,我们称之为共享区域。 * 对于共享区域,我们必须要实现上锁(线程的互斥访问)。 * * 对于共享变量,要完全放在同步区域当中,否则就可能会出现 * 问题。(存在修改共享变量) */ package day18; public class BuyTicket { public static void main(String[] args) { Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); // 设置线程的名字 t1.setName("张三"); t2.setName("李四"); t3.setName("王五"); t1.start(); t2.start(); t3.start(); } } class Ticket implements Runnable { private int num = 100; // 任意对象都可以充当共享区域的锁。 private Object lock = new Object(); // 因为任意对象都可以充当锁的角色,因此,我们没有必要 // 单独创建一个对象锁,使用当前对象this来充当锁就 // 可以了。 @Override public void run() { // while (num > 0) { // synchronized (lock) { // String name = Thread.currentThread().getName(); // System.out.println(name + "抢到第" + num + "张票"); // num--; // } // } // 虽然可以这样实现,但是这样做,完全退化成单线程。 // synchronized (lock) { // while (num > 0) { // String name = Thread.currentThread().getName(); // System.out.println(name + "抢到第" + num + "张票"); // num--; // } // } while (true) { synchronized (this) { if (num > 0) { String name = Thread.currentThread().getName(); System.out.println(name + "抢到第" + num + "张票"); num--; } else { break; } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } /* * 声明同步方法,使用synchronized修饰。同步方法的整个 方法体都是共享区域,都是互斥的进行访问的。 * 对于实例方法,使用当前对象this充当锁。 对于静态方法,使用当前类型的Class对象充当锁。 */ public synchronized void f() { } public synchronized void g() { } public void k() { synchronized (this) { } } public void n() { synchronized (lock) { } } }
package day18; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BuyTicket2 { public static void main(String[] args) { } } /* * Lock与synchronized * Lock是JDK1.5新增的内容,更加贴近与面向对象化。 * synchronized如果使用break,或者产生了一个未捕获的异常, * 同步区域的锁可以自动得到释放,但是Lock不能。为了确保Lock * 能够一定被解锁,将unlock方法写在finally语句块中。 */ class Ticket2 implements Runnable { private int num = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { // 加锁 lock.lock(); if (num > 0) { // 输出一大堆 num--; } else { break; } } finally { // 解锁,在finally中调用,确保锁能够 // 被解开。 lock.unlock(); } } } }
10、死锁
? 当两个或多个线程同时拥有自己的资源,而相互等待获得对 方资源,导致程序永远陷入僵持状态,这就是死锁。
? 当多线程并发访问共享数据时,使用同步操作可以避免多线 程并发修改带来的危害,但同时有可能会产生死锁。
/* * 死锁 * 死锁就是两个线程分别拥有自己的资源,而想要获得对方的资源, * 从而处于无限的僵持与等待之中。 */ package day18; public class DeadLock { public static void main(String[] args) { Fighter f1 = new Fighter("张三"); Fighter f2 = new Fighter("李四"); Thread t1 = new Thread(() -> { f1.hold(f2); }); Thread t2 = new Thread(() -> { f2.hold(f1); }); t1.start(); t2.start(); } } class Fighter { private String name; public Fighter(String name) { this.name = name; } public synchronized void hold(Fighter f) { System.out.println(name + "抓住了" + f.name); System.out.println(name + "等待" + f.name + "的放手"); // 等待着对方先放开自己 f.loose(this); // 然后自己再放开对方 this.loose(f); } public synchronized void loose(Fighter f) { System.out.println(name + "放开了" + f.name); } }
11、等待与唤醒
? 在多线程通信时,在某些特定条件下,我们需要线程做出一 定的“让步”,否则就很容易造成双方(或多方)进行僵持 状态,进而形成死锁。
sleep方法虽然能使当前线程阻塞,但是sleep方法不会释放其 所占有的任何“锁”。而且,也不能保证线程苏醒后,条件就 一定会得到满足。
我们可以使用以下方法实现线程的阻塞,并且令线程暂时释放 “锁”资源。以下方法都是在Object类中声明的(这意味着什
么?)。
? wait 令当前线程等待,直到另一个线程调用为该对象调用 notify或notifyAll方法。当前线程必要拥有该对象的锁。当调 用wait方法后,线程会释放掉其占有的锁,并处于等待队列中。
? notify 唤醒等待该对象锁的一个线程,如果有多个线程处于 等待中,仅唤醒一个。具体哪一个,取决于底层的实现(这
又意味着什么?)。
? notifyAll唤醒等待该对象锁的所有线程。
说明: wait, notify与notifyAll方法调用时,当前线程一定要拥 有对象的锁。否则将会引发IllegalMonitorStateException异常。
package day18; import java.util.ArrayList; import java.util.List; public class Product { public static void main(String[] args) { Worker w = new Worker(); Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { w.product(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { w.consume(); } }); t1.start(); t2.start(); } } class Worker { private List<String> list = new ArrayList<>(); public synchronized void product() { if (list.size() == 3) { System.out.println("仓库已满,生产阻塞。"); try { // 释放掉当前占用的锁资源,使自身处于阻塞队里当中。 // 调用该方法的线程必须要具有锁资源,否则就会产生异常 // (IllegalMonitorStateException)。 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { list.add(""); // 通知(唤醒)之前处于阻塞队列的线程。 notifyAll(); } } public synchronized void consume() { if (list.size() == 0) { System.out.println("仓库已空,消费阻塞。"); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { list.remove(0); notifyAll(); } } }
package day18; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Product2 { public static void main(String[] args) { Worker2 w = new Worker2(); Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { w.product(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { w.consume(); } }); t1.start(); t2.start(); } } /* * Condition与Lock联合使用。 */ class Worker2 { private List<String> list = new ArrayList<>(); private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void product() { try { lock.lock(); if (list.size() == 3) { System.out.println("仓库已满,生产阻塞。"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } else { list.add(""); // condition.signal(); condition.signalAll(); } } finally { lock.unlock(); } } public void consume() { try { lock.lock(); if (list.size() == 0) { System.out.println("仓库已空,消费阻塞。"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } else { list.remove(0); condition.signalAll(); } } finally { lock.unlock(); } } }