1. 继承Thread类
public class SyncThread extends Thread{private int count = 0;@ Overridepublic synchronized void run() {for (int i = 0; i < 5; i++) {try {System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));} catch (InterruptedException e) {e.printStackTrace();}}}
}
2. 实现Runnable接口
public class SyncThread implements Runnable{private int count = 0;@ Overridepublic synchronized void run() {for (int i = 0; i < 5; i++) {try {System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));} catch (InterruptedException e) {e.printStackTrace();}}}
}
3. 实现Callable接口
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {SyncThread syncThread = new SyncThread();CallableAndFutureTest callableAndFutureTest = new CallableAndFutureTest();FutureTask task = new FutureTask<>(callableAndFutureTest);Thread thread1 = new Thread(task);thread1.start();System.out.println(task.get()); //获取call()方法返回值}
}class SyncThread implements Callable {@Overridepublic String call() throws Exception {String str="";for (int i=0;i<10;i++){str+=String.valueOf(i);Thread.sleep(100);}return str;}
}
1. synchronized关键字
1. 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码;
2. 修饰方法,被修饰的方法称为同步方法,其作用的范围是整个方法;
2. Lock锁
1. 锁类型
可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 先进先出
不公平锁:可以插队
读写锁:读的时候可以一起读,写的时候必须同步写
2. Lock是接口
ReentrantLock实现类`:可重入锁,就是说某个线程已经获得某个锁,再次获取时不会出现死锁
ReentrantReadWriteLock实现类:含有readLock()和writeLock()两个锁,将读写进行了分离
3. lock()、trylock()、tryLock(long time, TimeUnit unit)、readLock().lock()、writeLock().lock()
lock()
public void run() {l.lock();for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + ": ");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}l.unlock();}
trylock()
if (l.tryLock()) {System.out.println(Thread.currentThread().getName() + "获取锁");for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + ": ");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}l.unlock();} else {System.out.println(Thread.currentThread().getName() + "未获取锁");}
** tryLock(long time, TimeUnit unit)**
if (l.tryLock(1000, TimeUnit.MILLISECONDS)) {System.out.println(Thread.currentThread().getName() + "获取锁");for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + ": ");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}l.unlock();} else {System.out.println(Thread.currentThread().getName() + "未获取锁");}
readLock().lock()和writeLock().lock()
rwl.readLock().lock();try {long start = System.currentTimeMillis();while(System.currentTimeMillis() - start <= 1) {System.out.println(thread.getName()+"正在进行读操作");}System.out.println(thread.getName()+"读操作完毕");} finally {rwl.readLock().unlock();}
3. volatile关键字
大家都知道volatile的主要作用有两点:保证变量的内存可见性、禁止指令重排序
1. 保证变量的内存可见性
在理解 volatile 的内存可见性前,我们先来看看这个比较常见的多线程访问共享变量的例子。
public class VolatileExample {public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();while(myThread.isFlag()){System.out.println("主线程访问到 flag 变量");}
}class MyThread extends Thread {public static boolean flag = false;public void run() {flag = true;}
}
执行上面的程序,你会发现,控制台永远都不会输出 “主线程访问到 flag 变量” 这句话。我们可以看到,子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子。
那么,我们思考一下为什么会出现这种情况呢?这里我们就要了解一下 Java 内存模型(简称 JMM)。
Java内存模型(JMM)
- 所有的共享变量都存储于
主内存
- 每一个线程还存在自己的
工作内存
,用于保留被线程使用的变量的工作副本- 线程对变量的所有的操作(读,取)都必须在
工作内存
中完成,而不能直接读写主内存
的变量。- 不同线程之间也不能直接访问对方
工作内存
中的变量,线程间变量的值的传递需要通过主内存
中转来完成。
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。
正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题。
那我们要如何解决可见性问题呢?接下来我们就聊聊内存可见性以及可见性问题的解决方案。
内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
可见性问题的解决方案
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
使用volatile关键字
class MyThread extends Thread {public static volatile boolean flag = false;public void run() {flag = true;}
}
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制
告知其他线程该变量副本已经失效,需要重新从主内存中读取。
嗅探机制工作原理
每个处理器通过监听在总线上传播的数据
来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改
,就会将当前处理器的缓存行设置无效状态
,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。
2. 禁止指令重排序
为了性能,从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:
为了更好地理解重排序,请看下面的部分示例代码:
int a = 0;// 线程 A
a = 1; // 1
flag = true; // 2// 线程 B
if (flag) { // 3int i = a; // 4
}
单看上面的程序好像没有问题,最后 i 的值是 1。但是为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序。假设线程 A 在执行时被重排序成先执行代码 2,再执行代码 1;而线程 B 在线程 A 执行完代码 2 后,读取了 flag 变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。那么如何程序执行结果正确呢?这里仍然可以使用 volatile 关键字。
这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。
synchronized的多线程通信
wait()
:wait()方法可以让当前线程释放对象锁并进入阻塞状态
notify()
:notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列
notifyAll()
:notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列lock的多线程通信
await()
:wait()方法可以让当前线程释放对象锁并进入阻塞状态
signal()
:notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列
signalAll()
:notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列BlockingQueue
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。
1. 概念
每个线程都会有属于自己的本地内存,在堆中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JVM管理控制写会到主内存中。
2. ThreadLocal与Synchronized的区别
3. 使用ThreadLocal实例
ThreadLocal>对象存放一个变量,如果需要存放多个变量,就需要多个ThreadLocal>对象
public class MyDemo01{ThreadLocal t1 = new ThreadLocal<>();private String content;private String getContent(){ return t1.get();}private void setContent(String content){t1.set(content);}public static void main(String[] args){MyDemo01 demo = new MyDemo01();for(int i = 0; i<5; i++){Thread thread = new Thread(new Runnable(){@Overridepublic void run(){//每一个线程存一个变量,过一会儿再来取出这个变量demo.setContent(Thread.currentThread().getName() + "的数据");System.out.println("------------------");System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());}});}}
}
4. ThreadLocal结构
Thread、ThreadLocal、ThreadLocalMap、Entry的原理
- 一个线程内部都有一个ThreadLocalMap
- 一个ThreadLocalMap里面存储多个Entry
- 一个Entry存储一个键值对,即线程本地对象ThreadLocal(key)和线程的变量副本(value)
5. ThreadLocal为什么是弱引用
- key使用强引用:当ThreadLocalMap的key使用强引用时,无法回收堆中的ThreadLocal。因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除ThreadLocalMap强引用的key,那么堆中的ThreadLocal不会被回收,导致Entry内存泄漏
- key使用弱引用:当ThreadLocalMap的key使用弱引用时,可以回收堆中的ThreadLocal。由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null时,在一下次ThreadLocalMap调用set()、get()、remove()方法的时候会被清楚value值的。
1. synchronized作用在方法
常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
2. synchronized作用在代码块
1. monitorenter指令
线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor
的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其
他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获
取monitor的所有权。
2. monitorexit指令
线程执行monitorexit指令时尝试获取monitor的所有权,过程如下:
执行monitorexit的线程必须是object ref所对应的monitor持有者。指令执行时,monitor的进入
数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个
monitor阻塞的线程可以尝试去获取这个monitor的所有权。
3. synchronized可以修饰静态方法和静态代码块吗?
synchronized可以修饰静态方法,但不能修饰静态代码块。
参考文章
1. ReentrantLock结构
ReentrantLock的lock()和unlock(),调用了Sync的acquire()和release(),所以通过对Sync的acquire()和release()重写,可以实现ReentrantLock的lock()和unlock()。
对Sync的acquire()和release()进行不公平锁重写,那么ReentrantLock实现的就是不公平锁。
对Sync的acquire()和release()进行公平锁重写,那么ReentrantLock实现的就是公平锁。
2. Sync和AQS
AQS 即 AbstractQueuedSynchronizer 的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列
是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列
是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾, AQS 所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
1. 为什么使用线程池
降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。
提高响应速度:直接从线程池中获取,而不是创建线程再执行
提高线程可管理性:使用线程池也可以统一分配调优监控
2. 线程池参数
设corePoolSize=5,workQueue.size=5,maxinumPoolSize = 10
- 现在来第1、2、3、4、5个任务,corePoolSize足够,所以可以直接创建线程。
- 再来第6、7、8、9、10个任务,corePoolSize满了,所以后面来的任务都得在workQueue排队
- 再来第11、12、13、14、15个任务,corePoolSize和workQueue都满了,所以需要创建workQueue里面排队的线程(FIFO)
- 后面再来任务,workQueue.size和maxinumPoolSize都满了,所以使用Handler拒绝
注意:最大线程数 = maxinumPoolSize.size;最大任务数 = maxinumPoolSize.size+ workQueue.size
3. 线程池的拒绝策略
1. AbortPolicy:丢弃任务,抛出异常。
2. DiscardPolicy:丢弃任务,不抛异常。
3. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
4. CallerRunsPolicy:由调用线程处理该任务。
4. 线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应的一个任务的限制。所以我可以认为线程池的本质就是线程复用。
5. 线程池状态
RUNNING:接收新任务,能处理队列任务
SHUTDOWN:不接受新任务,能处理队列任务
STOP:不处理队列任务,不处理队列任务,中断所有正在处理的任务
TIDYING:如果所有的任务都已终止了,调用 terminated() 方法进入TERMINATED 状态。
TERMINATED:什么都不做
说一说sleep()和wait()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
说一说join()方法
当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。从而实现子线程先执行,主线程再执行。