把程序执行路径只有一条的环境称为单线程环境
把程序的执行有多条路径的环境称为多线程环境
一、线程
1.进程
学习线程要先了解进程 因为线程依赖于进程
进程:正在运行的应用程序。
电脑上可以有多个进程,但在某个时间点上单核CPU只能执行一个进程。尽管人们感觉多个进程是在同时进行,但实际上只是单核CPU在多个进程间进行高速的切换,人们不能感觉出来。
多进程的意义:提高CPU的利用率
2.线程
线程依赖于进程,进程是线程的容器。进程开启后,它会执行很多个任务,每一个任务就被称为线程 人们感觉多个任务同时执行使用为线程是具有随机性的,它会抢占CPU的执行权,谁抢到,CPU在某个时刻就执行谁(但也只能执行一个)
进程是拥有资源的基本单位,线程是CPU调度的基本单位 操作系统在调度时的基本单位是进程间的多个线程而不是进程程序
二、并发和并行
1.并发:
指物理上的同时发生 同一时间间隔执行 应用程序能够交替执行不同任务 只是切换得太快 我们察觉不到 是不同实体上的多个事件 在单处理器和多处理器都存在
2.并行:
指逻辑上的同时发生 应用程序能够同时执行不同任务 如走路看手机 是同一个实体的多个事件 在多处理器存在
JVM 是多线程 至少两个线程 一个线程是主线程(main方法),还有一个垃圾回收线程
三、java实现多线程
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java不能直接调用系统功能,但可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。
1.创建多线程的方法A
1.定义一个类继承Thread类
2.重写Thread类中的run()方法
3.创建我们写的类的对象
4.开启分线程
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread();
//开启分线程,不是调用run方法,这只是代表 你在使用一个对象调用一个方法,让这个方法执行而已,没有开启线程
//开启分线程的正确方式是,调用start()开启线程,由分线程调用run方法
th.start();
//同一个线程不要多次开启 ,否则会报异常
//获取线程名称 该方法获取的是默认的线程名
th.getName();
//设置线程名
th.setName("a线程");
//Thread类中的静态方法 获取当前线程对象 再获取主线程名称
Thread t = Thread.currentThread();//获取当前线程对象
t.getName();//main
//也可以设名字
t.setName("主线程");
}
}
A.线程调度模型
分时调度模型:所有线程轮流使用CPU使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型 让优先级高的线程有更高几率使用CPU使用权(低优先级也可以使用,只是几率更低) 如果多个线程优先级一样,线程的执行就是随机抢占
java使用的是抢占式调度模型
B.设置线程优先级
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread();
MyThread mh = new MyThread();
//获取线程的优先级
int p = mt.getPriority();//默认优先级 NORM_PRIORITY 5
int p = mh.getPriority();//默认优先级 NORM_PRIORITY 5
//设置线程的优先级 Thread设有各个优先级的常量 也可以设整数 1-10 不能超过其范围
mt.setPriority(Thread.MAX_PRIORITY); //最大10
mH.setPriority(Thread.MIN_PRIORITY);//最小1
}
}
C.线程休眠
(让线程处于阻塞状态)
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
Thread.sleep(3000);//让当前线程休眠3000毫秒 异常只能抓取,不能抛出 父类没有抛出
}
需要一个有参构造
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread("分线程");//通过构造给线程设名字
mt.start();//3秒后执行分线程
Thread.sleep(3000);//3秒后执行主线程
}
}
D.线程加入
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread("分线程1");
MyThread mh = new MyThread("分线程2");
MyThread mr = new MyThread("分线程3");
//现在三个线程是并发执行
mt.start();
mh.start();
mr.start();
//串行 让多个线程挨个顺序执行
线程启动之后 这个线程执行完后其它线程再执行
mt.start();
mt.join();//分线程1执行完后其它线程再执行
mh.start();
mh.join();//分线程2执行完后其它线程再执行
mr.start();
mr.join();//分线程3执行完后其它线程再执行
}
}
E.礼让线程 效果相当不明显
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
//每个线程执行一次
Thread.yield();
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread();
MyThread mh = new MyThread();
}
}
F.守护线程
正常主线程执行完毕后必须分线程也执行完毕才会结束程序
将分线程设为守护线程后 主线程执行完毕 分线程必须结束,无论有没有执行完毕
守护线程适合做一些辅助性工作如垃圾回收 资源管理等
在Daemon线程中产生的新线程也是Daemon的。
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread();
MyThread mh = new MyThread();
mt.setDaemon(true);//设为守护线程
mh.setDaemon(true);//设为守护线程
mt.start();
mh.start();
System.out.println("主线程先执行完了");
}
}
G.打断阻塞线程interrupt
当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞
class MyThread extends Thread{
@override
public void run(){
//这个run方法就是需要分线程来执行的代码,一般耗时的操作就放到这里,让分线程执行,不会阻塞主线程的执行
mt.sleep(5000);//5秒后执行该线程
}
}
public class Test{
public static void main(String[] args){
MyThread mt = new MyThread();
MyThread mh = new MyThread();
mt.interrupt();//打断阻塞线程 不是5秒后执行
}
}
2.开启多线程的方式2
1.创建一个类,实现Runnable接口,重写该接口中的run方法
2.创建Thread对象,将Runnable接口的子类对象传递进来
Runnable 任务 该接口应该由那些打算通过某一线程执行其实例的类来实现
3.调用start()方法开启进程
class MyRunnable implements Runnable{
@Override
public void run(){
Thread.currentThread().getName();
System.out.println("分线程执行");
}
}
public class Test{
Public static void main(String[] args){
MyRunnable mr = new MyRunnable();
Thread th = new Thread(mr);
th.setName("分线程1");
th.start();
}
}
方式2 更加灵活 实现接口后还可以继承其它类
3.开启多线程的方式3
-
创建一个实现Callable接口的类 MyCallable(泛型根据返回值设置)
Callable 返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做
call
的方法 Callable 接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是Runnable 不会返回结果,并且无法抛出经过检查的异常。 -
创建该类对象mc
-
创建FutureTask类的对象ft并将mc作为参数传给FutureTask类的对象
-
创建Thread对象th,并将ft作为参数传进来
-
th.start();
class MyCallable implements Callable<Integer>{
//call方法即使线程要执行的方法
@Override
public Integer call()throws Exception{
int sum = 100+5;
return sum;
}
}
public class MyTest {
public static void main(String[] args) throws Exception{
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(mc);
Thread th = new Thread(ft);
th.start();
ft.get();//获取分线程运行完成后的运行结果
}
}
方式3可以获取分线程运行完成后的运行结果,可以抛出异常
四.线程安全
我们在实际网上访问多线程程序时,因为网络延迟,会出现重复票、0票、负数票
重复票 由于原子性导致 不是一个原子性(不可分割)的操作
0票、负数票 由线程的随机性导致的
1.出现数据安全问题需要三个前提条件
1.是多线程环境
2.存在共享数据
3.有多条语句在操作共享数据
前两个要求都是必须出现的,要避免数据安全问题只能避免第三个条件,可以使用同步代码块解决线程安全问题
2.synchronized(){}
//需要一个任意对象(多个线程共享同一个对象),当该对象在执行其中的代码块时,其它对象不能执行必须等候前一个对象执行完毕 如同一把锁将代码块锁住
将可能出现的代码块放到其中 这里面的代码块运行环境就是单线程环境 这里的代码块不能是一个死循环,否则一个线程进入此处代码块后一直不能结束,而其它进程也必须阻塞等待该线程
public class Text02 extends Thread {
static int piao = 4800;
static Object obj = new Object();
@Override
public void run() {
try {
this.sleep(50);
} catch (InterruptedException e) {
System.out.println("InterruptedException");
}
for (int i = 0;i<=5000;i++){
synchronized (obj){
if(piao>=1){
System.out.println(Thread.currentThread().getName()+piao--);
}else {
System.out.println("票已售空");
}
}
}
}
public Text02(String name) {
super(name);
}
public Text02() {
}
}
同步代码块比较耗费资源 效率低
方法上加上synchronized关键字我们叫做同步方法
当同步代码块与同步方法同时存在时,又会出现异常票
同步代码块使用任意对象作为锁对象
同步方法使用的锁对象不是任意对象,它用的锁是this,因为同步代码块的锁对象对同步方法没有用
静态同步方法使用的锁对象不是任意对象,它用的是类锁(当前类的字节码类型 MyRunnable.class)
我们使用上述三种锁之一,或将同时存在的锁的对象统一为一个就可以解决异常票、
我们之前说的那些容器有的是线程安全的有的是不安全的 线程安全是因为那些容器使用的是同步方法
3.lock(锁)类 ReentrantLock
在可能出现问题的代码块之前加上锁(lock.lock()) ,在这个代码块之后解锁(lock.unlock())
一般在try语句中上锁,finally语句中解锁 ,防止代码块出现异常后无法解锁
死锁 两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象
五、线程池
不同种类的线程间的通信
生产线程生产资源通知消费线程消费,消费线程消费资源后通知生产者生产
线程 间的等待唤醒机制
1.定义资源
2.要有生产线程
3.要有消费线程
4.测试类测试
发生线程安全 使用同步锁
1.线程间通信
等待唤醒
生产者生产资源后等待并通知消费线程消费
消费线程消费资源后等待并通知生产线程来生产
Object类中的方法
wait();
wait(long timeout);
wait();方法//一旦等待就必须释放锁 从哪里等待,被唤醒后就从哪里醒来
notify();//唤醒等待的线程,线程重新开始抢CPU执行权
notifyAll();
sleep和wait的区别
共同点:都可以让线程处于阻塞状态
不同点:1. sleep必须设置休眠量,wait可设可不设(没有唤醒就一直等)
2.sleep一旦休眠不会释放锁,wait阻塞会释放锁
2.内存可见性
JVM内存模型
synchronized 也可以但效率慢
volatile 修饰变量 可以解决内存可见性(共享变量在工作内存更新后能立刻刷回主内存)问题,无法解决原子性
3.原子性
CAS算法 比较并交换
是一种硬件对CPU并发访问的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
CAS 是一种无锁的非阻塞算法的实现。
CAS 包含了 3 个操作数:
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的
值,否则不会执行任何操作。
JDK1.5后java提供了原子性变量
AtomicBoolean 、 AtomicInteger 、 AtomicLong、AtomicReference
AtomicIntegerArray 、 AtomicLongArray
AtomicMarkableReference
AtomicReferenceArray
4.线程状态
新建:线程被创建出来
就绪:具有CPU的执行资格,但是不具有CPU的执行权
运行:具有CPU的执行资格,也具有CPU的执行权
阻塞:不具有CPU的执行资格,也不具有CPU的执行权
死亡:不具有CPU的执行资格,也不具有CPU的执行权
5.线程池
装有一定线程对象的容器
A.作用
程序启动新线程成本教高,而启用新线程可以很高地提高性能
如果我们创建一定数量的线程放入一个容器中,如果有任务就直接从该容器中调用线程,在线程执行完任务后不是立刻死亡,而是回到容器中等待调用。
Blocking Queue(阻塞队列)
JDK1.5之后,java支持内置线程池,不用手动实现
B.创建线程池方法
JDK1.5之后新增了一个Executors工厂来生产线程池,有如下几个方法
ExecutorService 线程池
1.根据任务数量来创建线程对应的线程个数
public static ExecutorService newCachedThreadPool ();
ExecutorService es = Executors.newCachedThreadPool();
MyRunnable mr = new MyRunnable();
es.submit(mr);
es.submit(new MyRunnable());
es.submit(new MyRunnable());
2.在这个线程池里面,提前创建指定个线程对象
ExecutorService es = Executors.newFixedThreadPool(3);
es.submit(new MyRunnable());
es.submit(new MyRunnable());
es.submit(new MyRunnable());
3.线程池中只有一个线程对象
ExecutorService es = Executors.newSingleThreadPool();
es.submit(new MyRunnable());
上面使用Runnable接口
下面使用Callable接口 获取线程执行之后的返回结果
ExecutorService es = Executors.newFixedThreadPool(3);
MyCallable mc = new MyCallable(10);//计算1-10整数的和
Future<Integer> f = es.submit(mc);
Integer it = f.get();
C、定时器
Timer工具类(util包下) TimerTask(定时器要执行的任务)
线程用其安排以后在后台线程中执行的任务,可安排任务执行一次,或者定期执行
Timer t= new Timer();
t.scheedule(new MyTimerTask(t),0);//立刻执行定时任务
t.scheedule(new MyTimerTask(t),2000);//2000毫秒后执行定时任务
在定时任务中t.cancel();//取消定时器
---------------------------------------------------------------------------
反复执行
MyTimerTask tt = new MyTimerTask();
t.schedule(tt,1000,2000);//1000毫秒后执行任务,每隔2000毫秒执行一次
tt.cancel();
t.cancel();
在指定日期执行定时任务
String date = "2019-08-30 14:36:00";
SimpleDateFormat sdf = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
Date d = sdf.parse(date);
t.schedule(new TimerTask(){
@Override
public void run(){
System.out.println("删除数据库,跑路")
}
},date);//指定日期2019-08-30 14:36:00指定该任务"删除数据库,跑路"