本文全文参考视频https://www.bilibili.com/video/av48144058内容编写
1、基本概念:程序、进程、线程
- 程序(programm):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
- 进程(process):程序的一次执行过程,或是正在运行的一个程序。
- 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
- 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
- 内存结构
- 并行与并发的理解
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
2、线程的创建和使用
- 有两种方法来创建一个新的执行线程。
2.1.新增线程方式一: 声明一个类是类Thread的子类。这类应重写类Thread的run方法。子类的一个实例可以被分配和启动。
- 例如,一个线程计算素数大于规定值可以写成如下:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
- 下面的代码将创建一个线程并开始运行:
PrimeThread p = new PrimeThread(143);
p.start();
如果直接调用run()方法,则不会创建新的线程,比如 p.run();
- 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
p.start();
p.start(); // 报错 IllegalThreadStateException
2.2.新增线程方式二:创建一个线程是声明一个类实现Runnable接口的其他方式。该类实现run方法。那类的一个实例可以分配,作为一个参数传递Thread时创建,并开始。
- 同样的例子在这个其他风格看起来像以下:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
- 下面的代码将创建一个线程并开始运行:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
2.3.Thread类的常用方法
1.start():启动线程并执行相应的run()方法
2.run():子线程要执行的代码放入run()方法中
3.currentThread():获取当前的线程 //静态方法
4.getName():获取子线程的名称
5.setName():设置子线程的名称
6.yield():调用此方法的线程释放当前CPU的执行权
7.join():在子线程1中调用线程2的join()方法,表示当执行到此方法时,线程1停止执行(阻塞)
8.sleep(long l):显示让当前线程睡眠(毫秒 1s = 1000ms)
9.isAlive():判断当前线程是否存活
10.getPriority():获取优先级,默认是5,最小是1,最大是10
11.setPriority():改变线程的优先级
12.线程通信的方法:
wait():等待
notify():唤醒等待的线程
notifyAll():唤醒所有等待的线程
2.4.线程的优先级
优先级高,并不会一定先执行,只是执行的概率变高。
高优先级线程–抢占–低优先级线程
例子:创建三个窗口卖票,总票数为100张
/**
* @Author Jerry
* @Description //例子:创建三个窗口卖票,总票数为100张
* @Date 19:52 2020/3/21
* @Param
* @return
**/
public class ThreadTest01 {
public static void main(String[] args) {
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread {
private static int ticket = 100;
public Window(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
- 该例子目前的写法存在线程安全问题。在学习线程的同步后就能解决
3、线程的生命周期
- 生命周期关注两个概念:状态、相应的方法
- 关注:状态a–>状态b:哪些方法执行了(回调方法)
- 某个方法主动调用:状态a–>状态b
- 阻塞:临时状态,不可以作为最终状态;死亡:最终状态。
线程的6种状态
- NEW(初始):线程被创建后尚未启动。
- RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片。
- BLOCKED(阻塞):线程阻塞于锁。
- WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。
- TERMINATED(终止):该线程已经执行完毕。
4、线程的同步
案例背景
例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式
1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
4.1.方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明
- 1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
- 2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求
- 多个线程必须要共用同一把锁。
public class ThreadTest01 {
public static void main(String[] args) {
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread {
private static int ticket = 100;
private static Object object = new Object();
public Window(String name) {
super(name);
}
@Override
public void run() {
while (true) {
synchronized (object){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
4.2.方式二:同步方法
@Override
public void run() {
show();
}
private static synchronized void show() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this --》Runnable private synchronized void show()
静态的同步方法,同步监视器是:当前类本身 --》Thread private static synchronized void show()
面试题:使用同步机制将单例模式中的懒汉式改写为线程安全的
public class BankTest {
}
class Bank {
public Bank() {
}
private static Bank instance = null;
// 方式一:同步方法
// public static synchronized Bank getInstance() {
// if (instance == null){
// instance = new Bank();
// }
// return instance;
// }
// 方式二:同步代码块
// public static Bank getInstance() {
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// return instance;
// }
// }
// 方式二性能优化:
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
4.3.方式三:Lock(锁)— JDK5.0新增
class LockWindow implements Runnable{
private int ticket = 100;
// 1.实例化ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 2.调用锁定方法
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
// 3.调用解锁方法
lock.unlock();
}
}
}
}
面试题:Java是如何解决线程安全问题的,有几种方式?并对比几种方式的不同
- 答:同步代码块(synchronized)、同步方法(synchronized)、Lock锁
面试题:synchronized和Lock方式解决线程安全问题的对比
练习题
package hjl.study.thread;
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("储户一");
c2.setName("储户二");
c1.start();
c2.start();
}
}
class Customer extends Thread {
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
// 存钱
public synchronized void deposit(double amt){
if (amt > 0) {
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":存钱成功。余额为:"+balance);
}
}
}
5、线程的通信
5.1.线程通信涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
5.2.说明:
- 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
面试题
- sleep() 和 wait()的异同?
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- 1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
案例一:使用两个线程打印1-100
/**
* 线程通信例子:使用两个线程打印1-100
* 线程一、线程二交替打印
**/
class Number implements Runnable {
private int number = 1;
@lombok.SneakyThrows
@Override
public void run() {
while (true) {
synchronized (this) {
// 唤醒一个线程(优先级高的先唤醒)
notify();
if (number <= 100) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
// 使得调用如下wait()的线程进入阻塞状态
wait();
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
案例二:线程通信的应用:生产者/消费者问题
import lombok.SneakyThrows;
/**
* 线程通信的应用:生产者/消费者问题
*
* 分析:
* 1.是否有多线程问题?是,生产者线程,消费者线程
* 2.是否有共享数据?是,店员(或产品)
* 3.如何解决线程的安全问题?同步机制,有三种方法
* 4.是否涉及线程内通信?是
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producter p1 = new Producter(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
/**
* 店员
**/
class Clerk{
private int productCount = 0;
// 生产产品
@SneakyThrows
public synchronized void produceProduct() {
if (productCount<20){
productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
// 生产者生产产品后,就可以唤醒消费者消费产品
notify();
} else {
wait();
}
}
// 消费产品
@SneakyThrows
public synchronized void consumeProduct() {
if (productCount>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
productCount--;
// 消费者消费产品后,产品减少,就可以唤醒生产者生产
notify();
} else {
wait();
}
}
}
/**
* 生产者
**/
class Producter extends Thread{
private Clerk clerk;
public Producter(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(this.getName()+":开始生产产品...");
while (true){
clerk.produceProduct();
}
}
}
/**
* 消费者
**/
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(this.getName()+":开始消费产品...");
while (true){
clerk.consumeProduct();
}
}
}
6、JDK5.0新增线程创建方式
6.1.新增线程方式三:实现Callable接口
理解案例一:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1.创建一个实现Callable的实现类
class NumThread implements Callable {
// 2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3.创建Callable实现类对象
NumThread numThread = new NumThread();
// 4.将此Callable实现类对象传递到FutureTask构造器中,创建FutureTask对象
FutureTask futureTask = new FutureTask<>(numThread);
// 5.启动线程
//public class FutureTask<V> implements RunnableFuture<V>
//public interface RunnableFuture<V> extends Runnable, Future<V>
//public class Thread implements Runnable {
new Thread(futureTask).start();
// 6.获取Callable中call()方法的返回值
// get()返回值即为FutureTask构造器参数Callable实现类重新的call的返回值
Object sum = futureTask.get();
System.out.println("总和为:"+sum);
}
}
6.2.新增线程方式四:使用线程池
理解案例一:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ExecutorsTest {
public static void main(String[] args) {
// 1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 设置线程池的属性
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(15); // 核心池的大小
service1.setMaximumPoolSize(20); // 最大线程数
// 2.执行指定线程的操作。
service.execute(new NumberThread()); // 适合使用于Runnable
service.execute(new NumberThread1()); // 适合使用于Runnable
// service.submit(); // 适合使用于Callable
// 3.关闭连接池
service.shutdown();
}
}
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}