什么是线程:
* 线程是进程执行的一条路径, 一个进程中可以包含多条线程,从而可以并发的执行
为什么使用:
Cpu和外设之间运行速度不匹配,提高cpu的利用率
将大任务划分多个小任务,从而提高运行效率
业务并行的需要(如有的编辑器边运行,边保存)
应用场景:
* 迅雷开启多条线程一起下载
* QQ同时和多个人一起视频
* 服务器同时处理多个客户端请求
线程特点:
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
* 并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
怎么实现:
1.继承Thread
* 定义类继承Thread
* 重写run方法
* 把新线程要做的事写在run方法中(在这个方法里实现自己的业务逻辑)
2.实现Runnable
* 定义类实现Runnable接口
* 实现run方法
* 把新线程要做的事写在run方法中
* 创建自定义的Runnable的子类对象
* 创建Thread对象, 传入Runnable子类对象
* 调用start()开启新线程, 内部会自动调用Runnable的run()方法
3.实现cannable接口,可以返回
多线程(实现Runnable的原理):
线程对象的构造函数传入runnable的引用,给该对象的成员变量赋值,当run方法运行时判断成员变量是否为空,若不为空则执行引用所对应的子类方法的run方
两种方法的优劣:
继承Thread
* 好处是:可以直接使用Thread类中的方法,代码简单
* 弊端是:如果已经有了父类,就不能用这种方法
* 实现Runnable接口
* 好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的
* 弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码相对复杂
怎么用多线程:
通过thread对象调用其start()方法;只是注意run方法里面的代码有没有调用其他对象,以及代码有没有设置同步
- 通过 声明thread对象调用start方法
- 通过匿名内部类的方式
线程类使用的常见方法:
成员方法:
getName()方法获取线程对象的名字
setName(String)方法可以设置线程对象的名字
setDaemon(), 设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出
join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
* join(int), 可以等待指定的毫秒之后继续
静态方法:
Thread.currentThread(), 获取当前线程的对象,主线程也可以获取22地方2.
Thread.sleep(毫秒,纳秒), 控制当前线程休眠若干毫秒1秒= 1000毫秒
thread.interrupt(),中断某个线程,有两种情况,一种是线程正在运行(Thread.currentThread().isInterrupted()这个状态默认为false,thread.interrupt()会将其设置成true,本线程中有这个判断,从而自己结束线程),另一种是线程在阻塞状态(首先被阻塞的会被唤醒,会将Thread.currentThread().isInterrupted()重新设置成false并抛出InterruptedException,让用户决定捕获异常后继续怎么操作)
什么是同步:
是多个线程间的关系,当其中的一个线程执行同步的代码块时,与其同步的其他线程不能执行其代码块.
应用场景:
当多线程并发, 有多段代码同时执行时, 操作相同的数据,引发线程安全,我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
怎么实现:
使用synchronize关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
多个同步代码块如果使用相同的锁对象, 那么他们就是同步的(相同代码块,不同代码块相互之间的),并且锁对象可以是任意但不能是匿名的;
一般 非静态同步函数的锁是:this
* 静态的同步函数的锁是:字节码对象
同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。
注意事项:
* 同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。
* 注意在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定统一个资源。
* 锁可重入.同一个线程,多次调用同步代码,锁定同一个锁对象,可重入(相当于一个同步方法)。其实重入锁就是避免调用时产生死锁的问题
* 子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法,可重入。
* 当同步方法中发生异常的时候,自动释放锁资源。不会影响其他线程的执行。注意,同步业务逻辑中,如果发生异常如何处理。
* 尽量在商业开发中避免同步方法,使用同步代码块。细粒度解决同步问题,可以提高效率(不需要同步的代码放在同步中)。
* 锁对象变更问题, 同步代码一旦加锁后,那么会有一个临时的锁引用应用锁对象,和真实的引用无直接关联,在锁未释放之前,修改锁对象引用,不会影响同步代码的执行(该同步代码块锁对象没有变)。
* 在定义同步代码块时,不要使用常量对象作为锁对象。
锁的种类:
*
synchronized(this)和synchronized加在方法都是锁当前对象。
* private Object o = new Object(); synchronized(o){}
* 静态同步方法,锁的是当前类型的类对象。
线程通信:
什么时候需要通信
* 多个线程并发执行时, 在默认情况下CPU是随机切换线程的
* 如果我们希望他们有**规律的执行**, 就可以使用通信, 例如每个线程执行一次打印
* 2.怎么通信
* 如果希望线程等待, 就通过锁对象调用wait()
* 如果两个方法之间希望唤醒等待的线程, 就调用通过锁对象notify();两个以上的线程随机唤醒,执行的结果就是随机的,而不是想要的效果.
* 这两个方法必须在同步代码中执行, 并且使用同步锁对象来调用
notifyAll()方法是唤醒所有线程
* JDK5之前无法唤醒指定的一个线程
* 如果多个线程之间通信, 需要使用notifyAll()通知所有线程, 用while来反复判断条件(当有唤醒时,因为所有的线程都被唤醒了,需要在回到判断条件处决定是否该它执行了,若是if唤醒了就会都执行)
因为锁对象可以是任意对象,Object是所有的类的基类,所以wait方法和notify方法都定义在Object这个类中
sleep方法和wait方法的区别
* a,sleep方法必须传入参数,参数就是时间,时间到了自动醒来
* wait方法可以传入参数也可以不传入参数,传入参数就是在参数的时间结束后等待,不传入参数就是直接等待
* b,sleep方法在同步函数或同步代码块中,不释放锁,睡着了也抱着锁睡
* wait方法在同步函数或者同步代码块中,释放锁
调用的对象不同,sleep是Thread类的静态方法,wait是Object的方法
wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁
其他相关对象应用
volatile应用:
* 通知OS操作系统底层,在CPU计算过程中,都要检查内存中数据的有效性。保证最新的内存数据被使用(若是变量没用指定volatile一般是线程直接从cpu的缓存中读取,若是其他线程已经改了内存中的数据,就会出现数据不一致的情况,一般用在非同步代码的情况下)。volatile只能保证可见性,不能保证原子性。如下面的例子t1线程是不会终止的
其实这个一个线程需要将缓存写入内存,一个线程能够将内存的最新值读取到缓存
volatile是具体怎么实现的?
要达到,更变值的线程,写到内存后,其他的线程需要感知到,并获得最新的,从而保证了数据正确性,java中提供了jmm规范由虚拟机来实现。
AtomicXxx:
AtomicInteger count = new AtomicInteger(0);//有多种相关的类型,保证多线程操作此数据的安全性
void m(){
for(int i = 0; i < 10000; i++){
/*if(count.get() < 1000)*/
count.incrementAndGet();
}
}
* 同步类型
* 原子操作类型。 其中的每个方法都是原子操作。可以保证线程安全。
门闩 - CountDownLatch
门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门闩数量大于 0,线程会阻塞等待。当线程调用 countDown 时,会递减门闩的数量,当门闩数
量为 0 时,await 阻塞线程可执行。
CountDownLatch latch = new CountDownLatch(5);//门闩定义,参数n表示加几个锁
void m1(){
try {
latch.await();// 等待门闩开放。其后的所有代码才能执行
}
void m2(){
for(int i = 0; i < 10; i++){
if(latch.getCount() != 0){
System.out.println("latch count : " + latch.getCount());
latch.countDown(); // 减门闩上的锁。
}
可以和锁混合使用,或替代锁的功能。在门闩未完全开放之前等待。当门闩完全开放后执行。 避免锁的效率低下问题。
什么是线程安全:
多线程并发操作同一数据时, 就有可能出现线程安全问题
怎么解决:
使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 不要多个线程一起操作
同步引发的问题:
死锁: 多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁
jdk1.5新特性:
Lock和synchronize主要区别,synchronize是由jvm帮忙实现的,而lock是由java.utils.currentl来实现的
Reentrantlock是包含sync工具类
private ReentrantLock r = new ReentrantLock();//锁对象
private Condition c1 = r.newCondition();
private Condition c2 = r.newCondition();
- 同步
* 使用ReentrantLock类的lock()和unlock()方法进行同步,相同reentrantlock对象的调用lock()方法定义的代码块是同步的
2.通信
* 使用ReentrantLock类的newCondition()方法可以获取Condition对象,condition对象有多个,不同的方法对应不同的condition对象
* 需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法
* 不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了
尝试锁
尝试锁,isLocked = lock.tryLock(); 如果有锁,无法获取锁标记,返回false。 如果获取锁标记,返回true
可以带有参数,阻塞尝试锁,阻塞参数代表的时长,尝试获取锁标记。 如果超时,不等待。直接返回。
isLocked = lock.tryLock(5, TimeUnit.SECONDS);
if(isLocked){
System.out.println("m2() method synchronized");
}else{
System.out.println("m2() method unsynchronized");
}
4.可打断
只能打断阻塞状态中的线程,用其他线程调用某线程的interrupt()后,该线程就会抛出InterruptedException类型的异常,从而获得执行权力,一般在抛出异常的catch中实现被打断后的相关逻辑
* 阻塞状态: 包括普通阻塞,等待队列,锁池队列。
* 普通阻塞: sleep(10000), 可以被打断。调用thread.interrupt()方法,可以打断阻塞状态,抛出异常。
* 等待队列: wait()方法被调用,也是一种阻塞状态,只能由notify唤醒。无法打断
* 锁池队列: 无法获取锁标记。不是所有的锁池队列都可被打断。
* 使用ReentrantLock的lock方法,获取锁标记的时候,如果需要阻塞等待锁标记,无法被打断。
* 使用ReentrantLock的lockInterruptibly方法(和lock一样只是可以被打断),获取锁标记的时候,如果需要阻塞等待,可以被打断。
- 公平锁
在没有使用公平锁的情况下,操作系统随机的选择线程执行;在使用同一公平锁对象的不同线程是根据等待的时长,时长较长的优先被执行,和非公平锁相比,公平锁是将所有的线程放入队列后,从队列前后依次执行;非公平锁加入队列之前都会尝试获得锁,若是获得了就会插队执行
// 定义一个公平锁
private static ReentrantLock lock = new ReentrantLock(true);
读写锁:如果同步代码块加的是读锁,线程之间可以并发执行,若是锁对象用的是写锁,线程之间就是同步运行的
使用场景,一般应用于写少读多的情况下,如对缓存的读写操作,写的时候,让读也变成了同步,从而避免读到了脏数据,但都是读的情况下就是异步的
Lock锁是真么实现的:主要通过其中的内部类AQS,由内部类提供的各种工具来实现
其中AQS中有一个双向链表的储存元素,来存储没有竞争到的线程,这些线程都封装成Node,
在线程尝试过程中没有获得锁的相关过程
程序是怎么保证同步执行:模拟三个线程争抢锁的过程,state是表示锁的状态,当它大于0(因为重入锁,在上面自增;当执行unlock的时候,就会将state复位成0,并且唤醒阻塞中的线程)的时候表示已经有线程获得锁了,其他线程就进入了双向队列中
ThreadLocal:
* 就是一个Map。key - 》 Thread.getCurrentThread(). value - 》 线程需要保存的变量。
* ThreadLocal.set(value) -> map.put(Thread.getCurrentThread(), value);
* ThreadLocal.get() -> map.get(Thread.getCurrentThread());
* 内存问题 : 在并发量高的时候,可能有内存溢出。因为threadlocal一般指定回收或是所有的线程都回收或是jvm退出时,其才会被回收
* 使用ThreadLocal的时候,一定注意回收资源问题,每个线程结束之前,将当前线程保存的线程变量一定要删除 。 ThreadLocal.remove();原因见下图
Condition是怎么完成阻塞的,有一个condition阻塞队列存入被阻塞封装了线程的node节点(不同的condition的阻塞队列是不一样的 ),是一个单向链表,同时其会释放锁,并唤醒所队列的某个线程继续执行,注意其与锁队列的区别
若是共享锁,就会唤醒所有的线程,共同获得锁,从而并行的执行
新笔记
Thread 是实现runnable接口的
Run方法是调用runnable的run方法的,所以没有通过继承thread方式重写run方法,二是通过实现runnable接口的方式,传递runnable的相关参数,就是执行重写runnable的run方法
为什么是start启动线程而不是run,run方法和其他普通方法是一样的,就不可能是启动线程的,因为真正的线程是操作系统提供,jvm根据不同的操作系统,启动操作系统的线程,然后又该线程区调用run方法里面的逻辑
为什么sleep wait join等阻塞状态的有编译时异常处理?
若是没有触发解除阻塞的动作,线程这些线程有可能永远停止不了
与上文中4、可打断 知识点相关
其实用的是复位的技术,调用该线程中断,当被中断线程从阻塞的进行唤醒同时获得锁时,知道自己被中断了,就会抛出异常,将线程状态复位成false,让捕获异常的catch进行相关的处理
为什么这样搞:
避免类似Linux里面kill操作,直接将线程终止,而不管线程处于什么运行状态,采取目前复位的方法,让被中断线程自己去处理完相应的逻辑
首先唤醒-复位-catch里执行
线程中断,也是调用操作系统的线程,去修改线程是否中断这个变量的值
线程复位:
由于锁是在对象上,就可以理解普通对象与class对象的范围和对象生命周期相关的
为了提升效率,会使用不同类型的锁,其实只有重量级锁是真正的加锁了,这样会导致没有获得锁的线程挂起,重新调度就会从用户态向内核态切换消耗性能,其他都是无锁