1、进程和线程概述
线程依赖于进程存在;
1.1 什么是进程
就是正在运行的程序,是系统进行资源分配和调用的地理单位;每一个进程都有它自己的内存空间和系统资源。
1.1.1 进程的三个特征
动态性:进程是运行中的程序,要动态的占用内存,CPU和网络等资源;
独立性:进程和进程之间是相互独立的,彼此有自己的独立内存区域;
并发性:假如CPU是单核,同一个时间段内其实内存中只有一个进程在被执行;CPU会分时轮询切换依次为每个进程服务,因为切换的速度非常快,给我们的感觉这些进程在同时执行,这就是并发性;
并行:同一个时刻同时有多个在执行;
1.1.2 多进程的意义
可以提高CPU的使用率,单进程的计算机只能做一件事情,而我们现在的计算机都是可以做多件事情;在一个时间段内执行多个任务。
1.2 什么是线程
线程是进程中的一个独立执行单元,线程是属于进程的,一个进程可以包含多个线程,这就是多线程。
1.2.1 多线程的意义
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率;
程序的执行其实都是在抢CPU的资源,CPU的执行权;
多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权;
我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性;
1.2.2 线程有两种调度模型
分时调度模型:轮流使用CPU的使用权;
抢占式调度模型(JAVA使用这种):优先让优先级高的使用CUP线程,线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多的情况下才能体现;
2、Java程序的运行原理
由Java命令启动JVM,JVM启动就相当于启动了一个进程;
扩展1:JVM虚拟机的启动是单线程的还是多线程的?
多线程的;
因为是垃圾回收线程也要先启动,否则很容易会出现内存溢出;所以,JVM的启动其实是多线程的;
3、线程创建的方式
3.1 方式一:继承Thread类
-> 重写run()方法 -> 实例化线程实现类 -> 调用start()方法
优点:编码简单;
缺点:线程类已经继承了Thread类无法继承其他类了,功能不能通过继承扩展(单继承的局限性);
3.2 方式二:实现Runnable接口
-> 重写run方法 -> 实例化线程实现类 -> 实例化Thread(Runnable)-> 调用start()方法
优点:线程任务类只是实现了Runnable接口,可以继续继承其他类;
同一个任务对象可以被包装成多个线程对象;
适合多个相同的程序代码的线程去共享同一个资源;
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立;
可以放入线程池;
缺点:代码复杂一些,不能直接得到线程执行的结果;
3.3 方式三:实现Callable接口
-> 重写call方法 -> 用FutrueTask类包装,FutureTask就是一个Runnable -> 实例化Thread(Runnable)-> 调用start()方法
优点:线程任务类只是实现了Runnable接口,可以继续继承其他类;
同一个任务对象可以被包装成多个线程对象;
适合多个相同的程序代码的线程去共享同一个资源;
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立;
可以放入线程池;
可以返回结果,FutrueTask.get();
缺点:编码复杂
3.4 注意事项
一般来说,被线程执行的代码肯定是比较耗时的。
3.5 方法
#获取线程名 final String getName() #设置线程名 final String setName() #返回正在执行的线程 static Thread currentThread() #设置优先级 setPriority() #线程休眠 static void sleep(long millis) #加入线程,等待该线程终止; final void join() #暂停正在执行的线程,并执行其他线程;礼让线程 static void yield() #守护线程 final void setDaemon(boolean) #中断线程并抛出一个异常 void interrupt()
3.6 扩展:run方法和start方法的区别?
run方法仅仅是是封装被线程执行的代码,直接调用是普通方法;
start方法是注册线程,然后再由JVM去调用该线程的run方法;
4、线程同步
多个线程同时操作同一个共享资源,可能出现线程安全问题(并发);
4.1 方式一:同步方法
synchronized 关键字声明方法上;在实例方法中默认this作为锁对象;在静态方法中默认类名.class字节码作为锁对象;
弊端:锁太多东西;
4.2 方式二:同步代码快(常用)
格式:
void test(){ synchronized( 对象 ) { 需要同步的代码块 } }
//对象:定义一个把锁,可以是任意对象
4.3 lock显示锁
创建一把锁reentrantLock;
#加锁
lock();
#释放锁
unlock();
4.4 死锁
是指两个或以上的线程在争夺资源的过程中,发生的一种相互等待的现象;
同步代码块嵌套出现问题;
5、线程通信
等待唤醒机制,多个线程因为在同一个进程中,所以互相通信比较容易的;
注意事项:
线程通信一定是多个线程在操作同一个资源才需要进行通信;
线程通信必须先保证线程安全,否则毫无意义,代码也会报错;
通信方法:
#Object类 wait();//等待,立即释放锁;将来醒过来是这边醒来的 notify();//唤醒单个线程;唤醒并不代表有执行权,还是要抢CPU执行权 notifyAll();//唤醒所有线程
6、线程生命周期
新建(NEW):线程刚被创建,但是并未启动。还没调用start方法;
就绪:调用了start方法;
运行:有执行资格,有执行权;
阻塞:由于一些操作让线程处于了该状态。没有执行资格,没有执行权;而另一些操作却可以把它给激活,激活后处于就绪状态;
死亡:线程对象变成垃圾,等待被回收;
7、线程组
ThreadGroup,默认所有线程都在同一个组;
8、线程池
其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源;
好处:
降低资源的消耗:减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
提高响应速度:不需要频繁的创建线程,如果有线程可以直接用,不会出现系统僵死;
提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机);
实现:
A:创建一个线程池对象,控制要创建几个线程对象:static ExecutorService newFixedThreadPool(int n);
B:这种线程池的线程可以执行:实现Runable者Callable接口的线程对象;
C:调用 submit()方法;
D:结束的话调用 shutdown();
9、Volatile关键字
作用:多线程下变量的不可见性;只保证数据的可见性;
原因:每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。每个线程是在自己的工作内存中操作共享变量的;
解决方法有两种常见方式:
方式一:加锁:1.线程获得锁,2.清空工作内存,3.从主内存拷贝共享两边最新的值到工作内存,4.执行代码
方式二:对共享变量用volatile关键字修饰;
保证原子性方法:
加锁;悲观锁
原子类;乐观锁(CAS)
10、并发包
在实际开发中如果不需要考虑线程安全问题,大家不需要做线程安全,因为如果做了反而性能不好;
但是开发中有很多业务是需要考虑线程安全问题的,此时就必须考虑了,否则业务出现问题;
原子类 - java为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择使用;
#ConcurrentHashMap
线程安全,综合性能较好;对比HashMap,线程不安全,效率高;Hashtable线程安全,效率差,被淘汰了;
分段式锁,只锁自己操作的元素位置,建议并发环境下使用;
#CountDownLatch
允许一个或多个线程等待其他线程完成操作,再执行自己;
相比Object的wait,notify,这个可以执行多步再唤醒;
#CyclicBarrier
某个线程任务必须等待其他线程执行完毕以后才能最终触发自己执行;
可以实现多线程中,某个任务在等待其他线程执行完毕以后触发,循环屏障可以实现达到一组屏障就触发一个任务执行;
#Semaphore
(发信号)的主要作用是控制线程的并发占锁数量;
可以控制线程并发占锁的数量;
#Exchanger
(交换者)是一个用于线程间的协作的工具类
11、定时器
是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在JAVA中,可以通过Timer和TimerTask类来实现定义调度的功能;
#Timer类 Timer(); schedule(TimerTask task,long delay); schedule(TimerTask task,long delay,long period); #TimerTask类 abstract void run(); boolean cancel(); #开发中Quartz是一个完全有JAVA编写的调度框架