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();
}
}
}