JavaSE内容整理十(多线程方面知识总结)

一、进程、线程、多线程概念
进程:正在执行的程序称作一个进程。进程负责了内存空间的划分。它是系统进行资源分配和调度的一个独立单位。
线程:线程在一个进程中负责了代码执行。
多线程:在一个进程中有多个线程同时在执行不同的任务。
注意:1.运行任何一个java程序,jvm在运行的时候都会创建一个主线程执行main方法中所有代码。
2.一个java程序中至少有两个线程,一个是主线程负责main方法代码执行;一个是垃圾回收器线程,负责了回收垃圾。
多线程的优缺点:
(1)多线程的弊端
增加了cpu负担
降低了一个进程中线程的执行效率
引发了线程安全问题
出现了死锁现象
(2)多线程的好处
多线程最大好处在于可以同时并发执行多个任务;
多线程可以最大限度地减低CPU的闲置时间,从而提高CPU的利用率。

二、创建两种线程的方法
1、继承Thread类
声明形式:继承Thread类,重写run方法
class MyThread extends Thread {
//把自定义线程的任务代码写在run方法中
public void run( ) {
/* 覆盖该方法*/
}
}
调用线程:
MyThread thread = new MyThread();
thread.start();

注意:
(1)一个线程一旦开启,那么线程就会执行run方法中的代码,run方法千万不能直接调用,如果直接调用就相当于调用了一个普通方法而已,并没有开启线程。
(2)启动线程(调用start方法),线程将等待调度(到底什么时候被调度,不一定要看当前的操作系统分配资源的情况);调度后,自动调用其run方法,run方法是等着被自动调用的。
案例:
public class MyPrinterThread extends Thread {

public MyPrinterThread(String name) {
	super(name);
}

//重写run()方法,run方法的方法体就是线程的执行体
public void run(){
	for (int i = 0; i < 100; i++) {
		System.out.println(this.getName()+"i"+i);
	}
}

}
public class TestThread {
public static void main(String[] args) {
// 创建线程
MyPrinterThread t1 = new MyPrinterThread(“thread1”);
MyPrinterThread t2 = new MyPrinterThread(“thread2”);
/*
* 1、启动线程,线程将等待调度(到底什么时候被调度,不一定要看当前的操作系统分配资源的情况);
* 调度后,自动调用其run方法,run方法是等着被自动调用的 2、创建两个线程后发现,不同的线程对象可以调用同一个方法。
/
t1.start();
t2.start();
// 如果以前没有线程我肯定是一步一步来执行,先创建对象,执行t1.run在执行t2.run
System.out.println(“main方法的最后一条语句”);
/

* 不同的机器运行的结果是不一样的,每次都是随机的,再运行一次还是不一样, 不是像以前一样把这一段的代码执行,
* 再去执行下一段的代码。它们现在是完全并行的两个任务,可以看到它们在交替执行。
*/
}
}

2、实现runnable接口
声明形式:
class MyThread implements Runnable{
public void run( ) {
/* 实现该方法*/
}
}
调用方式:
MyThread r = new MyThread();
//创建一个线程作为外壳,将r包起来,
Thread thread = new Thread®;
thread.start();

注意:Runnable接口的存在主要是为了解决Java中不允许多继承的问题。

案例:
public class PrintRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// thread当前线程的名字
System.out.println(Thread.currentThread().getName()+“i=” + i);
}
}
}

public class TestRunnable {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
PrintRunnable pr2 = new PrintRunnable();
Thread t1 = new Thread(pr1,“线程1”);
Thread t2 = new Thread(pr2,“线程2”);
t1.start();
t2.start();
}
}

三、Thread类中的常用方法
1、线程状态
在这里插入图片描述
(1)创建:新创建了一个线程对象,此时它仅仅作为一个对象实例存在, JVM没有为其分配CPU时间片和其他线程运行资源。
(2)可运行(就绪状态):线程对象创建后,其他线程调用了该对象的start()方法。这时,线程已经得到除CPU时间之外的其它系统资源,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会。
(3)运行:就绪状态的线程获取了CPU执行权,执行程序代码。
(4)阻塞状态: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
(5)死亡:线程执行完它的任务时,由JVM收回线程占用的资源。

2、构造方法
在这里插入图片描述
3、常用方法:
在这里插入图片描述

4、常用的静态方法:
在这里插入图片描述
在这里插入图片描述
5、常用方法解析:
(1)优先级
线程优先级用1~10 表示,10的优先级最高,默认值是5或继承其父线程优先级;优先级高的,获取的可能性则高。
在这里插入图片描述
案例:t1.setPriority(1);t2.setPriority(10);
(2)sleep方法:
使线程停止运行一段时间,此时将处于阻塞状态。阻塞的时间由指定的毫秒数决定
在这里插入图片描述
(3)yield()方法:
出让时间片,但不会阻塞线程,转入可运行状态。
在这里插入图片描述
(4)sleep()和yield()对比
在这里插入图片描述
(5)join()方法:
阻塞当前线程,强行介入执行。
在这里插入图片描述
(6)setDaemon方法:
1.要将某个线程设置为后台线程的话,必须在它启动之前调用setDeamon方法;
2.我们自己创建的线程,叫前台线程也叫用户线程。后台线程创建的都叫后台线程
3.前台线程执行结束,整个进程就结束
在这里插入图片描述

四、线程同步
1、线程安全问题分析:
需求:模拟三个窗口同时在售50张票
问题1:为什么50张票被卖出了150次?
在这里插入图片描述
出现原因:因为num是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,三个线程对象就会有三份。
解决方案:把num票数共享出来给三个对象使用。使用static修饰。

问题2:刚刚的问题是解决了,但是为什么会出现两个窗口卖同一张票呢?–出现了线程安全问题
在这里插入图片描述
解决方案:一个时间只能由一个线程操作内容—线程同步机制
在这里插入图片描述

2、线程安全出现的根本原因
(1)存在两个或者两个以上的线程对象,而且线程之间共享着一个资源
(2)有多个语句操作了共享资源
3、线程同步的概念
(1)多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入线程“同步”机制,即各线程间要有先来后到;
(2)同步就是排队:几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作;
(3)确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。

4、线程同步机制方式一:同步代码块
语法:
在这里插入图片描述
同步代码块要注意的事项:
1、任意一个对象都可以作为锁对象
2、在同步代码块中调用sleep方法并不会释放锁对象的
3、只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率。
4、多线程操作的锁对象必须是唯一共享的,否则无效。

6、线程同步机制方式二:同步方法
语法:
在这里插入图片描述
同步方法要注意:
1、如果是一个非静态的同步方法的锁对象是this对象;如果是静态的同步方法的锁,对象是当前函数所属的类的字节码文件(Class对象)。
2、同步方法的锁对象是固定的,不能由你来指定。
推荐使用:同步代码块
原因:
1、同步代码块的锁对象可以由我们随意指定;同步方法的锁对象是固定的,不能指定。
2、同步代码块可以很方便的需要被同步的范围,同步方法必须是整个方法的所有代码都被同步了。

7、线程同步的优缺点
优点:解决了线程安全问题、对公共资源的使用有序化
缺点:性能下降;会带来死锁

五、死锁
1、概念:线程死锁指的两个线程互相持有对方依赖的共享资源,造成无限阻塞
2、死锁案例:电池和遥控器(张三拿电池,李四拿遥控器,谁也不肯放手)
在这里插入图片描述
注意:这种情况一定会发生吗?不一定,当张三快速的执行完毕时
3、死锁出现的根本原因:(1)存在两个或者两个以上的线程 (2)存在两个或者两个以上的共享资源

4、死锁解决方案:没有方案。只能尽量避免发生

六、线程通信
1、概念:线程通讯指的是多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用。(一个线程完成了自己的任务的时候,要通知另外一个线程去完成另外一个任务)
2、常用方法:
(1)wait(): 等待–如果线程执行了wait方法,那么该线程会进入等待的状态,等待状态下的线程必须要被其他线程调用notify方法才能唤醒。
(2)notify(): 唤醒–唤醒等待线程其中的一个。
(3)notifyAll() : 唤醒所有等待的线程。

3、wait与notify方法要注意的事项:
(1) wait方法与notify方法是属于Object对象 的。
(2) wait方法与notify方法必须要在同步代码块或者是同步方法中才能使用。
(3)wait方法与notify方法必需要由锁对象调用。

3、生产者和消费者案例:
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

七、线程生命周期

在这里插入图片描述

八、线程池
1、并发队列:分两种阻塞队列和非阻塞队列。而线程池底层是阻塞队列
(1)队列特点:先进先出
(2)入队:假设队列的容量的长度为10
非阻塞队列:当队列中满了的时候,放入第11条数据,那么此条数据丢失。
阻塞队列:当队列满了的时候,进行等待,什么时候队列中有出队的数据,那么第11个再放进去。
(3)出队:
非阻塞队列:如果现在队列中没有元素,取数据,得到的是null。
阻塞队列:等待,什么时候放进去,再取出来。

(4)非阻塞队列案例

在这里插入图片描述
(5)阻塞队列案例
在这里插入图片描述

2、线程池概念
我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。
3、为什么使用线程池?
在一个应用程序中,需要多次使用线程意味着多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。在Java中,内存资源是及其宝贵的,所以,提出了线程池的概念。
创建—》就绪状态–》运行–》死亡
3秒 1秒 2s
几个没线程没关系,但是高并发系统中
线程池:10线程–100个任务 (10个线程执行1-10,然后就接着执行10-20 。10线程相当于共享的资源,不用创建,使用完不用销毁,这样就节省了创建和销毁的时间)
eg:学校100台电脑,有第101个学生等待,有学生退电脑然后借电脑。

4、线程池底层实现原理
(1)原理图
在这里插入图片描述

(2)构造方法参数说明:
1.corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
2.maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
3.keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
4.unit:指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
5.workQueue:线程池中的任务队列.
常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。

(3)常用方法:
1.int getCorePoolSize():返回核心线程数。
2. int getPoolSize():返回池中当前的线程数。
3.BlockingQueue getQueue():返回此执行程序使用的任务队列。
4.void shutdown():关闭线程池
5.void execute(Runnable command):执行任务

(5)案例:

在这里插入图片描述

5、线程池分类
在这里插入图片描述
在这里插入图片描述
1、newCachedThreadPool创建一个可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。 线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。
案例:
在这里插入图片描述
2、newFixedThreadPool创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。定长线程池的大小最好根据系统资源进行设置。
创建方式:
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
在这里插入图片描述
3、newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
创建方式:Executors.newSingleThreadExecutor() ;
在这里插入图片描述

4、newScheduleThreadPool创建一个定长线程池,支持定时及周期性任务执行
在这里插入图片描述在这里插入图片描述

6、实现Callable接口–创建线程的第三种方式
注意:1.底层Callable的接口
在这里插入图片描述
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的,方法可以有返回值,并且可以抛出异常。但是Runnable不行。
2.Callable需要依赖FutureTask,用于接收运算结果。一个产生结果,一个拿到结果。FutureTask是Future接口的实现类。

在这里插入图片描述

7、线程池的方法
(1)execute(Runnable)以异步的方式执行它,这种方式没有办法获取执行Runnable之后的结果,如果你希望获取运行之后的返回值,就必须使用 接收Callable参数的execute() 方法.
在这里插入图片描述
(2)submit(Runnable)会返回一个Future 对象用于判断Runnable任务是否结束执行
在这里插入图片描述
(3)submit(Callable):Future对象中可以获取Callable的返回值
在这里插入图片描述
(4)inVokeAny():返回集合中某个Callable对象的结果,而且无法保证调用之后返回的结果是集合中的哪个Callable结果,只知道它是这些Callable中的一个。

在这里插入图片描述
九、信号量
信号量为多线程协作提供了更为强大的控制方法。信号量是对锁的扩展。锁一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
eg:车位是共享资源,每辆车好比是一个线程,看门人是信号量。
public class SemapDemo implements Runnable{
//信号量对象,只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
public static void main(String[] args) {
final SemapDemo semapDemo = new SemapDemo();
//固定数目线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//模拟20个客户端访问
for (int i = 1; i <= 20;i++) {
//执行
executorService.execute(semapDemo);
}
//退出线程池
executorService.shutdown();
}
public void run() {
try {
//获取许可
semp.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId()+",done!");
//访问完,释放,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后一直线程阻塞
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

十、线程并发控制方法
1、volatile
volatile:用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。
特点:
(1)volatile具有可见性、有序性,不具备原子性。
(2)volatile不会让线程阻塞,响应速度比synchronized高。
原子性:原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成没问题,如果只执行了一部分,你得撤销(即事务中的回滚)已经执行的部分。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序(指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度),但是却会影响到多线程并发执行的正确性。
volatile适用场景
适用于读多写少的场景。可用作状态标志。JDK中volatile应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile。

2、synchronized
3、信号量
4、ThreadLocal 线程局部变量:ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
5、ReentrantLock重入锁
在这里插入图片描述
1.重入锁分为公平锁和非公平锁;公平锁保证在锁等待队列中, 各线程是公平的, 因此不会存在插队情况, 对锁的获取总是先进先出。非公平锁没有这个保证, 后申请锁的线程可能会先获得锁.
2.方法
(1)lock() : 等待获得锁, 等待过程中不会响应中断异常, 等到执行unlock释放锁时, 再响应中断
(2)lockinterruptibly() : 与lock的唯一区别在于,等待过程中会响应中断,抛出中断异常
(3)tryLock() :tryLock尝试在指定时间内获取锁, 并在等待时间内可以响应中断异常.
获取成功返回true, 否则返回false.

注意:还有其它方法ReadWriteLock读写锁、condition对象等

十、定时任务
1、Timer是Java最早提供的一个任务调度器,它可以支持定时任务和重复任务的调度
在这里插入图片描述
2、参数TimerTask是一个抽象类,通过一个run()抽象方法来定义任务执行的内容。
在这里插入图片描述
3、案例
public class TimerScheduleTest {
public static void main(String[] args) {
Timer timer = new Timer();//创建对象
//安排指定的任务在指定的时间开始执行任务,并开始重复固定的工作
timer.schedule(new NormalTask(), addOneSecond(new Date()), 1000);
}
//当前时间加上5秒
public static Date addOneSecond(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, 5);
return calendar.getTime();
}
}
class NormalTask extends TimerTask {
@Override
public void run() {
System.out.println(“正常的任务”);// 定时器执行的任务
}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值