Java线程简单讲解(尚硅谷视频笔记)
基础知识
程序:为了完成特定功能,用某种语言编写的一组指令的集合
进程:正在执行的程序
线程:就是进程的进一步划分,程序内部的执行路径
一个进程里有多个线程。在一个进程中,所有的线程都要共享 方法区、堆。而 虚拟机栈和程序计数器 则是每个线程独立拥有的
所以线程之间的通信很方便,但是由于共享的问题,会导致了一些安全隐患
单核CPU:当有许多线程请求时,只会执行一个线程,其他的挂在那里,等当前的线程执行一段时间后,再去处理下一个线程
一个 .java 程序有至少有三个线程,main主线程,gc垃圾回收线程,异常线程。如果发生了异常,会影响主线程。
并行:多个CPU同时执行多个任务
并发:一个CPU(时间片)同时执行多个任务,秒杀、多个人做同一个事情
优点:提高程序的响应,提高CPU利用率,将复杂的进程分为多个线程,独立运行,利于理解和修改,就像我们设计程序一样
用到多线程的地方:
GC垃圾回收:程序需要同时执行多个任务时。如Java程序在运行时,会用内存,所以就要跟进一个GC垃圾回收的线程,不然会导致内存的溢出
加载内容:程序需要实现一些需要等待的任务时,就好比我们在加载网页的时候,可用创建多个线程,一个用来加载文字,一个用来加载图片这样。
线程的创建及使用
下面展示的就是一个线程,但注意不是一个多线程!!!
public class ThreadTest {
public static void main(String[] args) {
System.out.println("main方法开启之后就是一个线程");
}
}
关于Thread类
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现
java.lang.Thread类
Thread 里面有个 run() 方法,这个是线程开启之后Thread类自动调用的方法。
Thread 里面的 start() 方法是开启线程的方法,注意不是 run()
实现多线程的方式一:继承Thread类:
1.继承 lang包下的Thread类
2.重写里面的 run() 方法
3.在主函数中调用继承类中的 start() 方法,开启线程
// 继承Thread类
class MyThread extends Thread{
// 重写 run 方法
@Override
public void run() {
// 遍历100次
for (int i = 0; i < 100; i++) {
if(Thread.currentThread().toString().contains("Thread-1"))
System.out.println(Thread.currentThread().getName() + "****3333****");
else
System.out.println(Thread.currentThread().getName() + "****2222****");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 这里开启了两个线程 myThread 和 myThread1
// 创建继承自 Thread 类的实例
MyThread myThread = new MyThread();
// 启动当前线程并调用run()方法
myThread.start();
// 创建继承自 Thread 类的实例
MyThread myThread1 = new MyThread();
// 启动当前线程并调用run()方法
myThread1.start();
// myThread.run(); // 这样调用,其实就只是一个类的实例调用里面的方法而已,等于在 main 线程中执行。运行此程序通过线程名可以发现
// 遍历100次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "****2222****");
}
}
}
注意,每个继承类实例的start()方法只能调用一次,因为在其内部的源码中,会先判断当前线程的状态
如果线程执行了,就会抛出一个 Illegal异常
如果要在创建第二个线程,只能在new一个继承类实例,调用start即可
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU
调度决定。
实现多线程的方式二:实现Runnable接口
1.创建一个实现了 Runnable 接口的类
2.实现里面的方法 run();
3.创建实现类的对象
4.将此对象传入 Thead() 构造方法中
5.start(); 开启线程
class MThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest01 {
public static void main(String[] args) {
MThread mThread = new MThread();
Thread t1 = new Thread(mThread);
t1.start();
}
}
这里可能会疑惑:Thread执行start()方法时,调用的是Thread里面的run()方法,那么是怎么执行Runnable实现类里面的run方法呢?
通过run()源码可知,里面有一个 target 值,用来进行判断是否为空,不为空则执行 target.run() 方法
而 target 是 Thread 定义的一个 Runnable 类型的空对象
Thead 类中有一个构造方法可以对 Runnable 类进行赋值,而赋值的对象就是 target 这个Thread定义的空对象
同时,Thread里面的run()方法也是继承 Runnable接口 得来的
所以当 target == null 时,就会执行 Thread类中实现 Runnable接口的 run() 方法
Thread类中的 run() 方法只会执行 target.run(),否则不会输出任何东西,可以通过源码得知。
所以这就是为什么,即使我们继承了 Thread类 也需要重写里面的 run() 方法
// Thread 里面的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
比较创建线程的两种方式:
开发中,优先选择 实现Runnable接口的方式
1. 实现的方式不会出现单继承的局限性
2. 实现的方式更适合来处理多个线程中有需要共享数据的清空
联系:public class Thread implements Runnable
相同点:两种方式都需要重写 run(),将线程要执行的逻辑声明在 run() 中
Thread 常用方法
start(); 启动当前线程,并调用 run(); 方法
run(); 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currenteThread(); 返回当前线程对象,静态方法
getName(); 获取当前线程的名字
setName(); 执行当前线程的名字
yield(); 释放当前CPU的执行权
join(); 在线程A中调用线程B的join()方法,此时A线程就进入堵塞状态,直到B线程执行结束后,A线程才会结束阻塞状态
stop(); 已经过时,强制结束当前线程
sleep(Long milliTime); 使当前线程进入睡眠状态,单位是毫秒,睡眠状态指阻塞状态
isAlive(); 判断当前线程是否存活。
关于线程的调度问题
每个线程都有一个优先级,线程创建时继承自父线程的优先级
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略
这是Thread类中定义的常量
MAX_PRIORITY = 10;
MIN_PRIORITY = 1;
NORM_PRIORITY = 5; --> 默认优先级
获取与设置优先级
getPriority();
setPriority();
但是优先级并不代表一定会优先执行 优先级高的线程,优先级低的线程才能执行。这只是说执行优先级高线程的概率比较多而已。
线程的分类
守护线程和用户线程。当用户线程结束了,守护线程也结束了
线程的生命周期
新建(NEW): 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪(RUNNABLE):处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行(RUNNING):当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞(BLOCKED):在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡(DEAD):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
图片来源于->尚硅谷
线程的同步
线程的安全问题:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
下面这个程序就是有线程安全问题的
class Ticket implements Runnable{
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class TicketTest{
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
第一种方式:同步代码块
synchrodized(同步监视器){
// 需要被同步的代码
}
关于锁的介绍
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意
必须确保使用同一个资源的 多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
解决了线程安全问题的代码
class Number implements Runnable{
private static int i = 100;
@Override
public void run() {
while (true){
synchronized (this){
if(i > 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
i--;
}else {
break;
}
}
}
}
}
public class Communication {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
说明:
1. 操作共享数据的代码,即为需要被同步的代码
2. 共享数据:多个线程共同操作的变量。
3. 同步监视器:锁。任何一个对象都可以充当锁,多个线程必须使用同一个锁,this代表当前对象,也可以充当锁
4. 同步的方式:解决了线程了安全问题
操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低
关于两种实现方法,运用 同步代码块的注意点
Thread 继承
慎用this当锁,因为每new一个此继承类的对象,都会调用那个类的对象,所以不是唯一的。可以用 类名.class 充当锁
Runnable 继承
可以用this当锁,因为底层调用都是用 Runnable 的实现类调用run()方法的,所以对象是唯一的
第二种方式:同步方法
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
关于两种实现方法,运用 同步方法的注意点
Thread 继承
privite static synchronized void show(){// 方法体}
此方法必须是静态的,不然多个对象调用的方法还是独立,不是唯一的
这个同步监视器的锁就是 类.class 也就是当前类本身,因为这个是唯一的
Runnable 继承
privite synchronized void show(){// 方法体}
这个就是一个同步方法,它的锁就是 this
单例模式-懒汉式(线程安全)
class Bank{
private Bank() {
}
private static Bank instance = null;
public static Bank getInstance(){
/**
* 这里是实现了单例模式
* 因为线程会有效率问题,就比如,一开始有一个线程已经拿到了实例,后面每个线程还要挨个进来判断,但最后都是要返回一个已经创建的实例
* 这样就会影响效率,所以,我们直接把判断条件加在外面,这样每个线程在进行操作的时候,都先进行判断,没有就直接绕过了
* 从而避免了占用线程锁在归还锁的效率问题
* @return 一个 Bank 的对象
*/
if(instance == null){
synchronized (Bank.class){
return new Bank();
}
}
return instance;
}
}
关于线程的死锁问题
死锁的理解
不同的线程分别占用对方需要的同步资源不放弃,都在等在对方放弃自己需要的同步资源,就形成了线程的死锁。
就好比,一个男的和一个女的,互相暗恋,但又都等着对方先表白,最后谁都没表白,消失在人海当中。
说明
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
我们使用同步时,要避免出现死锁
解决方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
Lock 锁 java5.0新增
java.until.concurrent.Locks.Lock 接口是控制多个线程对共享资源进行访问的工具
ReentrankLock 类实现了 Lock,它拥有于 synchrodized 相同的并发性和内存语义
在实现线程安全的控制中,比较常用的是 ReentrankLock,可以显示加锁、释放锁
案例
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
private static int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(ticket);
ticket--;
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
new Thread(new Window()).start();
}
}
synchronized 和 lock 的异同
相同:二者都可以解决线程的安全问题
不同:
- synchrodized 机制在执行完相应的同步代码后会自动释放档同步监视器
- Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
练习程序
银行有一个账户
有两个储户分别向同一个账户存3000元,每次存1000,存3次,每次存完打印账户余额
问题:该程序是否有安全问题,如果有,如何解决
提示:
1.明确哪些代码是多线程运行代码,必须写入run()方法
2.明确什么是共享数据
3.明确多线程运行代码中哪些语句是操作共享数据的
扩展问题:可否实现两个储户交替存钱的操作
分析:
1. 是否有多线程问题? 是,两个出乎线程
2. 是否有共享数据? 有,账户(或账户余额)
3. 是否有线程安全问题? 有,因为有共享数据,并且两个储户都要操作这个共享数据,所以就会有安全问题
4. 需要考虑如何解决线程安全问题? 同步机制:有三种方式(同步代码块、同步方法、Lock锁)
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
public 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);
}
}
}
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);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(0);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
线程的通信
涉及到的三个方法:
wait(); 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify(); 一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被wait,就唤醒优先级高的
notifyAll(); 一旦执行此方法,就会唤醒所有被 wait 的方法
说明:
- 以上三个方法,必须使用在同步代码块和同步方法中
- 以上三个方法的调用者,必须是同步代码块或同步方法中的同步监视器
否则会出现异常 - 以上三个方法不是定义在 Thread 中的,而是定义在 Object 类中
线程通信的应用:经典例题【生产者/消费者问题】
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(20),如果生产者试图生产更多的产品,店员会叫生产者停一下,
如果店中有空位放产品了再通知生产者继续生产
如果店中没用产品了,店员会告诉消费者等一下
如果店中有产品了,再通知消费者来取走产品
分析:
- 是否是多线程问题? 是,生产者线程,消费者线程
- 是否有共享数据? 是,店员(产品)
- 如何解决线程的安全问题?同步机制,有三种方法
是否涉及线程的通信? 是
class Clerk{
// 这是公用的产品
private int productCount = 0;
// 同步方法,这样当生产者的线程获得CPU执行权的时候,消费者就进入了阻塞状态
public synchronized void productProduct() {
// 如果产品数量 < 20 就生产
if (productCount < 20){
// 先生产产品,再输出
productCount++;
// 输出
System.out.println(Thread.currentThread().getName() + "开始生产第:" + productCount + "个产品");
// 生产完产品后,唤醒在阻塞状态的消费者线程
notify();
}else {
// 如果产品数量 > 20 就陷入阻塞状态,直到消费者线程唤醒,或者产品数量再次小于20
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 同步方法,这样当消费者的线程获得CPU执行权的时候,生产者就进入了阻塞状态
public synchronized void consumeProduct() {
// 如果产品数量 > 0 就消费
if (productCount > 0){
// 输出
System.out.println(Thread.currentThread().getName() + "开始消费第:" + productCount + "个产品");
// 消费产品
productCount--;
// 消费完产品后唤醒生产者线程,当然也会出现产品超过20,这个时候即使唤醒了生产者线程,它也会立即进入阻塞状态
notify();
}else {
// 如果没有产品可以消费,那么就进入阻塞状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer extends Thread{
// 用来当锁对象
private Clerk clerk;
// 获取锁对象
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + "开始消费产品...");
while (true){
try {
// 消费产品的时间
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 调用店员类中的方法,开始生产产品
clerk.consumeProduct();
}
}
}
class Product extends Thread{
// 用来当锁对象
private Clerk clerk;
// 获取锁对象
public Product(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + "开始生产产品...");
while (true){
try {
// 生产产品的时间
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 调用店员类中的方法,开始生产产品
clerk.productProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
// 唯一的锁对象
Clerk clerk = new Clerk();
Product product = new Product(clerk);
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
product.setName("生产者");
c1.setName("消费者1");
c2.setName("消费者2");
product.start();
c1.start();
c2.start();
}
}
sleep() 和 wait() 的异同?
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
不同点:
- 两个方法声明的位置不同,Thread 类中声明 sleep(); Object 类中声明 wait();
- 调用的要求不同,sleep() 可以在任何需要的场景下使用,而 wait() 只能在 同步代码块或同步方法 中
- 关于是否释放同步监视器:如果两个方法都是用在 同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁
创建线程的方式三:实现 Callable 接口 JDK5.0 新增
Callable 接口比 Runnable 接口强大
call() 可以有返回值
call() 可以抛出异常
call() 可以声明泛型
普通方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class NumThread1 implements Callable{
@Override
public Object call() throws Exception {
// 将100以内偶数进行累加
int sum = 0;
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
sum += i;
System.out.println(sum);
}
}
// 返回结果
return sum;
}
}
public class ThreadNew1{
public static void main(String[] args) {
// 创建一个实现了Callable接口的类对象
NumThread1 numThread = new NumThread1();
// 创建FutureTask类对象,传入 numThread
FutureTask task = new FutureTask(numThread);
// 创建一个Thread类,构造方法中传入 task
Thread thread = new Thread(task);
// 开启线程
thread.start();
try {
// 获取返回结果
System.out.println("返回的结果为:" + task.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
利用泛型的方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1. 创建一个实现了 Callable 接口的类,这里采用了泛型<Integer> ,一般默认是没有泛型的
class NumThread implements Callable<Integer>{
// 这里一开始的返回值类型是 Object,这边我利用了泛型,所以需要修改,大家可以自行把泛型删除
// 2. 实现里面方法 call();
@Override
public Integer call() throws Exception {
// 将100以内偶数进行累加
int sum = 0;
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
sum += i;
System.out.println(sum);
}
}
// 返回结果
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
// 3. 创建一个 NumThread 对象实例
NumThread numThread = new NumThread();
// 4. 创建一个 FutureTask 类,里面传递 NumThread 的对象实例
// 注意,这里我也用了泛型,因为要配合上面的泛型使用
// 如果这里不加泛型为报错,因为在底层源码里,这个方法也用了泛型
FutureTask<Integer> task = new FutureTask<Integer>(numThread);
// 5. 开启线程,为什么可以传递 FutureTask 的实例对象,是因为 FutureTask 也实现了 Runnable 方法
new Thread(task).start();
try {
// 6. 如果对返回结果不感兴趣可以跳过,这里的 get() 方法是获取 call() 方法的返回值
System.out.println("返回的结果为:" + task.get());;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
关于 Callable,需要借助于 FutureTask 类,如获取结果
Future接口
可以对具体Runnable、Callable任务的执行结果进行取消、查询是
否完成、获取结果等。
FutrueTask类是Futrue接口的唯一的实现类。
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
创建线程的方式四:使用线程池
线程池,顾名思义,就是已经创建好了许多的线程在一个容器里面,我们需要线程的之后直接从里面取出来即可,就不需要自己在创建了,大大提高了效率,注意获取用完之后需要进行关闭。
好处:
提高相应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
线程池中的属性,但是这种获取需要强转为 ThreadPoolExecutor 类,通过底层源码可知道,这个类的 父类实现了ExecutorService接口
corePoolSize:核心池的大小
maximumPoolSize:最大线程树
keepAliveTime:线程没有任务时最多保持多长时间结束
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 可以看到是哪个类创建了 service 这个对象,通过分析,发现是 ThreadPoolExecutor
System.out.println(service.getClass());
// 直接强转获取 ThreadPoolExecutor 类对象
ThreadPoolExecutor s1 = (ThreadPoolExecutor) service;
// 设置线程池的属性
s1.setCorePoolSize(15);
// 2. 执行指定的线程的操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
service.execute(new NumberThread()); // 适用于 Runnable
service.execute(new NumberThread1()); // 适用于 Runnable
// service.submit(new NumberThread()); // 适用于 Runnable
// service.submit(Callable callable); // 适用于 Callable
// 3. 关闭连接池
service.shutdown();
}
}
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。