多线程
在了解多线程之前,先了解一些概念
概念了解
程序(program)
是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象
就表示还未启动
进程(process)
是程序的一次执行过程,或是正在运行的一个程序。是一个动态 的过程:有它自身的产生、存在和消亡的过程。——生命周期
正在启动的程序
程序是静态的,进程是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread)
一个进程中最少存在一个主线程。可以包含多个线程
若一个进程同一时间并行执行多个线程,就是支持多线程的
比如:360是一个程序,当启动之后就会产生一个进程
一个进程包含一个主线程从而产生多个线程。
360存在很多功能模块,可以多个功能同时使用,一个功能模块表示一个线程
线程对应JVM
简单提到
- (硅谷的30天java面向对象章节)前面提到过一次JVM,这里都是简单介绍,下面我的这一专栏文章会进行硅谷的视频拓展下去
- 堆:存放new出来的对象
- 栈:存放变量
- 方法去:存放静态,常量,静态代码块
这里看到有两个栈,我们说的栈就是虚拟机栈
虚拟机栈可以有多个,每一个线程就对应一个虚拟机栈。我们栈的指向堆中信息
所以说当多个线程,线程的资源是进行共享的
单核和多核CPU
现在我们的CPU估计都很厉害了,四核,八核多的是
一个计算机上我们经常去启动多个程序,一个程序表示一个进程,每个进程都存在主线程
一核
执行多个程序,虽然看起来,都在运行。
其实是一个核在来回切换不同的进程进行操作,只是计算机操作的速度很快,肉眼看起来认为是多个程序同时执行
多核
一个核对应一部分程序,减轻了不少的压力,提高效率
并行和并发
- 概念类似于单核和多核
- 并行:多个CPU同时执行不同的任务。不如多个人做不同的事情
- 并发:一个CPU做多个任务,秒杀多个用户需要进行操作,一个管理来负责多个用户任务
为什么需要多线程
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方 法),
肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
这里都说多线程没有单线程执行速度快了,为什么还要使用多线程
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和 修改
何时需要多线程
程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。
需要一些后台运行的程序时。比如:后台的数据加载
线程的创建(1)
非多线程的java代码
public class NoThreadCode {
//一个main方法表示主线程
public static void main(String[] args) {
// 下面代码的执行无论怎么运行都是按照约定好的顺序一条线执行的,多以不是不多线程
System.out.println("开始");
Two();
One();
System.out.println("结束");
}
public static void One(){
System.out.println("开始1");
}
public static void Two(){
System.out.println("开始2");
}
}
继承(Thread)创建多线程
示例
这里看注释,看一些我标记的序号可能好一点
package Thread;
public class ThreadOneTest {
// 2.在主线程中进行遍历打印100
public static void main(String[] args) {
// 3.通过调用自定义的线程类
ExtendsThreadTest ett = new ExtendsThreadTest();
// 4.开启该线程,也是打印100以内的数字
ett.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:"+i);
}
}
}
//1.创建一个线程实现
//继承Thread类
//重写run方法
//在主线程中进行调用start()方法开启线程
class ExtendsThreadTest extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("自定义线程:"+i);
}
}
}
这里我截取一段的控制台运行结果
发现他们的执行,并不是像以往一样一个方法执行完毕之后,才去执行另一个方法
而是两个方法交叉执行,两个线程在一起抢占资源,谁抢占到资源,谁先执行
Thread类常见方法
方法 | 描述 |
---|---|
start() | 两个功能:开启一个线程,调用本类中的run函数 |
run() | 通常在子类中重写,表示该线程主要的执行功能 |
currentThread() | 静态代码,返回当前代码的线程 |
getName() | 获取线程名字 |
setName() | 设置当前线程名字 |
yieId() | 释放当前CPU资源 |
join() | 在a线程中调用线程b的join,a就会进入阻塞状态,知道b结束才结束阻塞状态 |
stop() | 执行此方法,将强制结束当前线程 |
sleep(Long millitime) | 让线程睡眠指定毫秒数,睡眠期间属于阻塞状态 |
isAlive() | 判断当前线程是否活着 |
如果上面我们没有调用start(),直接调用run()执行的结果就是按照顺序执行的,并没有开启多个线程。
小结
我们不能通过直接调用run()来代替start()
不可以重复的调用start(),start()源码中有一个判断。
每个线程之间交叉执行
线程的调度
如何调度
多个线程之间的调度,可以遵循优先级来进行
- 同优先级的线程组成(先进先出)使用时间片策略
- 高优先级。使用抢占式策略
- 并不是优先级越高,就优先执行,只能说执行的概率提高。进行抢占资源
策略
时间片:同等优先级线程之间的执行,抢时间片进行执行,多个线程相互谦让
抢占式:高优先级的线程进行抢占CPU资源
线程优先级
在Thread类源码中设置了三个优先级(最高)(默认)(最低)
关于优先级的方法使用
方法 | 描述 |
---|---|
getPriority() | 返回线程优先级 |
setPriority(int Priority) | 设置线程的优先级 |
优先级只能影响获得资源的概率,并非一定是高的优先级执行完毕才执行低优先级
线程创建(2)
硅谷的视频真的太良心了,大家一起加油学习
上面我们通过继承Thread类的方式创建了多线程,下面再介绍一种
package Thread;
//创建一个类,实现Runnable接口
//重写run方法。该线程的具体操作
public class RunableThread implements Runnable {
public void run() {
Thread thread = new Thread();
// 打印10遍当前线程名称加上编号
for (int i = 0; i < 10; i++) {
System.out.println(thread.getName()+":--"+i);
}
}
}
class MainThreadTest{
public static void main(String[] args) {
// 首先我们将线程类创建出来
RunableThread runT = new RunableThread();
// 使用Thread构造创建出一个线程,带入Runnable接口的实现
// 可以创建多个并行
Thread t1 = new Thread(runT);
Thread t2 = new Thread(runT);
t1.start();
t2.start();
}
}
第二种方式通过Runnable接口的实现方式结合Thread的构造函数带入,可以用来创建多个线程
相比较第一种,简便很多
在硅谷的课程中有一个买票的示例,非常细致,大家有兴趣可去了解一下啊
两种创建的方式对比
开发中:优先选择,实现Runnable接口的形式创建
原因:
-
实现的方式没有类的单继承,局限性
-
实现起来稍微简单
-
可以在多个线程中设置共享数据 (在接口实现类中创建一个静态变量,有它创建的多个线程,都可以使用该数据并同步进行操作)
java线程的分类
线程的声明周期
声明周期,之后很多地方都会出现这个概念,简单说:就是一个事物从创建到消亡的过程
要想实现多线程,必须在主线程中创建新的线程对象。
Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
java线程五种状态
新建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
就绪
处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
运行
当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
死亡
线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程之间状态的切换
新建到就绪很简单就是线程创建出来就执行线程就进入了就绪
在就绪和运行状态会因为资源的抢占来回切换。
阻塞,在一个线程阻塞状态就需要先解除阻塞才能正常使用
死亡就是线程的执行完毕
线程的同步(1)
主要就是为了解决线程的安全问题,下面先来演示一个不进行线程同步出现的错误
非同步买票
package Thread;
public class Threadticket {
public static void main(String[] args) {
RunableThreadTest rt = new RunableThreadTest();
// 模拟三方买票
// 在RunableTreadTest类中通过静态变量,设置的票是100张
// 每次run调用满足条件都会进行减少操作
Thread t1 = new Thread(rt);
Thread t2 = new Thread(rt);
Thread t3 = new Thread(rt);
t1.start();
t2.start();
t3.start();
}
}
//实现runnable接口的线程方式
class RunableThreadTest implements Runnable{
// 表示现在总共有100张票
static int ticketnum=100;
// 当前线程获取
public void run() {
Thread thread = new Thread();
while (true){
if(ticketnum>0){
// 这里我特意编写两个打印语句来进行查看
System.out.print(thread.getName()+"买到"+ticketnum+"号票");
ticketnum--;
System.out.println("剩余"+ticketnum);
}else {
break;
}
}
}
}
通过结果我们看到两个不同的线程,买到相同票号
仔细观察,发现4线程还没有执行完毕,3线程就已经开始执行了。
4线程刚做完打印语句和扣除票的数量操作,没有打印剩余票数。3线程就也进入了运行状态,打断4线程自己还在执行
出现了一个线程还没有做完操作,被另一个线程就抢占资源现象
同步代码块解决
语法
// 在此代码块中的代码,上了锁一样吗,中间代码执行不会被打断
synchronized (监控对象){
// 使用实现Runnable接口监控对象可以使用this来表示监控对象
//this当前类创建的对象,这个类也就创建一个对象,交给Thread类去构造实现
// 注意这里的继承方式不能使用this 可以使用反射中的类也是对象处理
// 继承实现类.class的形式
}
示例
package Thread;
public class Threadticket {
public static void main(String[] args) {
RunableThreadTest rt = new RunableThreadTest();
// 模拟三方买票
// 在RunableTreadTest类中通过静态变量,设置的票是100张
// 每次run调用满足条件都会进行减少操作
Thread t1 = new Thread(rt);
Thread t2 = new Thread(rt);
Thread t3 = new Thread(rt);
t1.start();
t2.start();
t3.start();
}
}
//实现runnable接口的线程方式
class RunableThreadTest implements Runnable {
// 表示现在总共有100张票
static int ticketnum = 100;
// 同步代码块需要使用多个线程同样的一个对象进行监控
Object obj = new Object();
public void run() {
Thread thread = new Thread();
while (true) {
// 使用同步代码块,只能等代码块中执行完毕后再能在抢占资源
// 为这个代码块上个锁,执行期间不会被占用
synchronized (obj) {
if (ticketnum > 0) {
// 这里我特意编写两个打印语句来进行查看
System.out.print(thread.getName() + "买到" + ticketnum + "号票");
ticketnum--;
System.out.println("剩余" + ticketnum);
} else {
break;
}
}
}
}
}
解决了问题
操作同步代码块的时候,相当于从多个线程回到了一个线程,其他的线程在等待,正在锁住的代码执行,执行效率会被降低
使用同步代码块来解决继承Thread的创建方式
因为Thread继承方式,需要多个线程共享数据,所以我们将多个线程需要的(共享数据)(共享锁)
使用static来处理即可,只要由实现类创建出来的都是共享static资源
同步方法
我们可以将通过代码块中的代码提成一个同步方法
符合面向对象,的调用
示例
package Thread;
public class ThreadSynMethod {
public static void main(String[] args) {
RunableThreadt rt = new RunableThreadt();
// 模拟三方买票
// 在RunableTreadTest类中通过静态变量,设置的票是100张
// 每次run调用满足条件都会进行减少操作
Thread t1 = new Thread(rt);
Thread t2 = new Thread(rt);
Thread t3 = new Thread(rt);
t1.start();
t2.start();
t3.start();
}
}
//实现runnable接口的线程方式
class RunableThreadt implements Runnable {
// 表示现在总共有100张票
static int ticketnum = 100;
public void run() {
while (true) {
// 使用同步方法
synMethod();
}
}
// 将代码块提成方法
// 默认的锁监视器 ==》this
public synchronized void synMethod(){
if (ticketnum > 0) {
// 这里我特意编写两个打印语句来进行查看
System.out.print(Thread.currentThread().getName() + "买到" + ticketnum + "号票");
ticketnum--;
System.out.println("剩余" + ticketnum);
}
}
}
如果想要继承Thread类的方式,使用同步方法
死锁问题
当我们多个线程进行使用,难免出现问题,死锁是一种
什么是死锁呢
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁
多个线程,一个a同步方法中在放入一个b同步方法,两个同步方法分别不同的锁
当a同步方法调用b同步方法时候b的锁被其他线程占用了,占用方是a同步方法调用b同步方法时候b的锁被其他线程占用了
这么一来,两个线程都进入了阻塞,谁都不能执行
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
package Thread;
public class DeadLocal {
public static void main(String[] args)throws Exception {
// 用来表示两个锁
final Object obj1 = new Object();
final Object obj2 = new Object();
// 创建一个线程,需要获取两个锁才能释放资源
new Thread(new Runnable(){
public void run() {
synchronized (obj1){
// 为了下面能获取资源,该线程在此处休眠一会
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":拿到了锁1"+obj1.toString());
objM(obj2);
}
}
}).start();
// 这里直接使用主线程调用同步方法 两个线程的锁相互锁住达到了死锁的情况
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+":拿到了锁1"+obj2.toString());
objM(obj1);
}
}
// 创建同步方法
public static void objM(Object obj){
synchronized (obj){
System.out.println(Thread.currentThread().getName()+":拿到了锁2"+obj.toString());
}
}
}
存在两种结果
发现存在两种情况
一:没有出现死锁,线程执行过快,睡眠之后又抢占到两个锁的资源
二:双方都存在对方一个锁,是放不出资源
死锁的避免
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
线程同步(2)
Reentrantlock
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。
语法
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
示例
package Thread;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
RunableThreadq rt = new RunableThreadq();
// 模拟三方买票
// 在RunableTreadTest类中通过静态变量,设置的票是100张
// 每次run调用满足条件都会进行减少操作
Thread t1 = new Thread(rt);
Thread t2 = new Thread(rt);
Thread t3 = new Thread(rt);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
//t3.start();
t1.start();
t2.start();
}
}
//实现runnable接口的线程方式
class RunableThreadq implements Runnable {
// 表示现在总共有100张票
static int ticketnum = 100;
// 每创建一个ReentranLock都表示一个锁
ReentrantLock lock = new ReentrantLock();
public void run() {
Thread thread = new Thread();
while (true) {
// 使用lock进行上锁
lock.lock();
if (ticketnum > 0) {
// 这里我特意编写两个打印语句来进行查看
System.out.print(Thread.currentThread().getName() + "买到" + ticketnum + "号票");
ticketnum--;
System.out.println("剩余" + ticketnum);
} else {
break;
}
// 所用lock进行开锁
lock.unlock();
}
}
}
我在敲示例的时候有的时候会把锁住的范围,和锁创建的范围搞混大家一定要多注意
synchronized与lock对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法 (在方法体之外)
线程通信
多个线程之前需要进行抢占资源锁,这个时候我想这样
当一个线程执行过之后我先等待一下,等别人也执行过我在去执行,从而不会说我都占用了
使用到的方法
方法 | 描述 |
---|---|
wait() | 当前线程进入阻塞状态,并释放同步锁资源这一点和sleep是一样的sleep阻塞也不释放资源 |
notify() | 唤醒其他wait的线程,如果有多个wait的线程,优先唤醒优先级别高的 |
notifyAll | 唤醒所有wait的线程 |
**wait():**令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行。
**notify():**唤醒正在排队等待同步资源的线程中优先级最高者结束等待
**notifyAll ():**唤醒正在排队等待资源的所有线程结束等待
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声明
示例
package Thread;
import java.util.concurrent.locks.ReentrantLock;
public class waitThread {
public static void main(String[] args) {
ThreadH th = new ThreadH();
Thread t1 = new Thread(th);
Thread t2 = new Thread(th);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class ThreadH implements Runnable {
static int a = 0;
ReentrantLock lock = new ReentrantLock();
public void run() {
// lock.lock();
while (true) {
synchronized (this) {
// 唤醒其他的线程
notify();
System.out.println(Thread.currentThread().getName() + ":" + a);
a++;
try {
// 我这个线程执行过了累了休息一会。
wait();
} catch (InterruptedException e) {
}
// lock.unlock();
}
}
}
}
看到线程进行了交替执行
上面看到我的注释中一开始使用的Reentrantlock类来进行的,但是后面取消了因为我报异常了
我去看来别人的博客,哈啊哈我是小菜鸡,
线程创建(3)
Callable接口
与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是 否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为 Runnable被线程执行,又可以作为Future得到Callable的返回值
示例
package Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws Exception {
// Callable接口可以和Runnable接口稍微不一样
// 需要结合Futrue接口使用
CallableImple ct = new CallableImple();
FutureTask future = new FutureTask(ct);
// 进行调用执行
Thread thread =new Thread(future);
thread.setName("thread");
thread.start();
// 该方法可以获取 Callable的call方法返回值
// 存在checked异常,进行声明抛出
Object o = future.get();
System.out.println("最终a的打印"+o);
// Thread thread = new Thread(ct);
}
}
//实现Callable来创建多线程
//重写call方法,该方法可以进行返回值和抛出异常啊
class CallableImple implements Callable{
static int a = 0;
public Object call() throws Exception {
while (true){
if(a<100){
System.out.println(Thread.currentThread().getName()+":"+a);
a++;
}else {
break;
}
}
return a;
}
}
线程池
什么是线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。
可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。
为什么使用线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
线程池的API使用
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:
真正的线程池接口。常见子类ThreadPoolExecutor
方法 | 描述 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable |
Future submit(Callable task) | 执行任务,有返回值,一般又来执行 Callable |
void shutdown() | 关闭连接池 |
Executors:
工具类、线程池的工厂类,用于创建并返回不同类型的线程池
方法 | 描述 |
---|---|
Executors.newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池 |
Executors.newFixedThreadPool(n) | 创建一个可重用固定线程数的线程池 |
Executors.newSingleThreadExecutor() | 创建一个只有一个线程的线程池 |
Executors.newScheduledThreadPool(n) | 创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。 |
示例
硅谷的线程池示例
跟着硅谷写示例
在这个调用基础上更换成连接池形式
package Thread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class waitThread {
public static void main(String[] args) {
ThreadH th = new ThreadH();
// 创建线程池放入10个线程
ExecutorService service = Executors.newFixedThreadPool(10);
// 传入实现类进行多线程的使用
service.execute(th);
service.execute(th);
service.shutdown();
// 下面的注释改为上面,来回切换
// Thread t1 = new Thread(th);
// Thread t2 = new Thread(th);
//
// t1.setName("t1");
// t2.setName("t2");
//
// t1.start();
// t2.start();
}
}
class ThreadH implements Runnable {
public void run() {
int a = 0;
while (a < 100) {
synchronized (this) {
notifyAll();
System.out.println(Thread.currentThread().getName() + ":" + a);
a++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
以往都是手动创建线程,有了线程池,通过线程池将实现类作为参数传入即可。
还可以进行更多的线程池配置
**扯淡:**我知道我自己写的这些特别烂,如果遇到我写的有些问题。大佬请指出,哈哈,一起努力好好进步吧55