1. 线程与进程
-
使用多线程提高程序的效率,每个线程互相独立运行,互不影响。
-
应用程序:可以执行的软件,QQ,微信
-
进程:就是正在运行的程序,是线程的集合,有独立的运行内存空间。在进程中一定有一个主线程,例如程序中的main函数
-
线程:就是正在独立运行的一个执行路径,一个独立的执行单元
-
多线程应用场景:多线程下载、QQ、爬虫、前端ajax(异步上传)、分布式job(需要同时一个执行多个任务调度)
使用多线程体现程序的效率。
2.创建线程有哪些方式
- 继承Thread类,重写run方法(就是线程要执行的任务或者要执行的代码)
- 实现Runnable接口,重写run方法
- 使用匿名内部类
- 实现callable接口
- 使用线程池创建线程
package cx;
//表示是一个线程类,继承Thread类
public class Demo extends Thread {
@Override
public void run() {
System.out.println("继承Tread类创建线程");
}
public static void main(String[] args) {
Demo t1 = new Demo();
//启动线程不是调用run方法,而是start方法
t1.start();
}
}
package cx;
//实现Runnable接口,重写run方法
public class Demo implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口");
}
public static void main(String[] args) {
Demo t1 = new Demo();
Thread thread = new Thread(t1);
thread.start();
}
}
package cx;
//匿名内部类
public class Demo{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名内部类");
}
});
thread.start();
}
}
使用继承Thread类还是使用Runnable接口好?
使用实现Runnable接口好,原因是实现了接口还可以继续继承,继承了类就不能再继承了。
3.获取线程对象以及名称 常用API
start() 启动线程
- currentThread() 获取当前线程对象
- getID() 获取当前线程ID Thread-0 该编号从0开始
- getName() 获取当前线程名字
- sleep(long mill) 休眠进程
- Stop() 停止进程
4…常用线程构造函数
- Thread() 分配一个新的Thread对象
- Thread(String name) 分配一个新的Thread对象,具有指定的name
- Thread(Runnable r) 分配一个新的Thread对象
- Thread(Runnable r,String name) 分配一个新的Thread对象
5.守护线程与非守护线程
守护线程:和main有关
用户线程:用户自己创建的线程。如果主线程停止掉,不会影响用户线程,非守护线程
gc线程:守护线程
守护线程有一个特征:和主线程一起销毁。
6.多线程的几种状态
新建状态、准备状态、运行状态、阻塞状态、死亡状态
7.join方法
join作用是让其他线程变为等待,t1.join(),让其他线程变为等待,直到当前t1线程执行完毕,才释放
thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如线程b中调用了线程a的join方法,直到a执行完毕,才会继续执行线程b
就是优先权的事
2.1线程安全
2.1.1什么是线程安全?
就是当多个线程访问某一个类(对象或方法)时,这个类(对象或方法)始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
2.1.2为什么有线程安全问题?
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
2.2 synchronized关键字
可以在任意对象及方法上加锁,而加锁的这段代码称为互斥区
或临界值
什么是互斥区?
就是多个处理器都可以访问一块共享的内存,但为了结果的正确性,一个处理器必须完成访问这块内存和某些相应的计算,另一个处理器才能访问这块内存并进行某些它相应的计算。
也就是说他们的访问一些规定好的范围的计算是不能同时发生的。
只有当前线程结束了,才轮到你来执行,不然一直把你排斥到外面。
什么地方考虑加锁
共享同一个全局变量的时候
什么是同步函数
同步函数就是在方法上加上synchronized关键字,使用的是this锁(证明方式:一个线程使用同步代码块(this明锁),另一个线程使用同步函数。如果两个线程不能实现同步,那么会出现数据错误
什么是静态同步函数
在同步函数加static
问:两个线程一个使用同步函数,一个使用静态同步函数能实现同步么?
不能,同步函数使用的是this锁,静态同步函数使用的是当前字节码文件
举个例子:
public class synchronizedDemo extends Thread{
private int a = 5;
public void run(){
a--;
System.out.println("当前线程名称:" + this.currentThread().getName()+" a = "+a);
}
public static void main(String[] args) {
synchronizedDemo synchronizedDemo = new synchronizedDemo();
Thread t1 = new Thread(synchronizedDemo,"t1");
Thread t2 = new Thread(synchronizedDemo,"t2");
Thread t3 = new Thread(synchronizedDemo,"t3");
Thread t4 = new Thread(synchronizedDemo,"t4");
Thread t5 = new Thread(synchronizedDemo,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
运行结果:
当在run()方法前加上synchronized关键字时即:
public synchronized void run(){
a--;
System.out.println("当前线程名称:" + this.currentThread().getName()+" a = "+a);
}
运行结果:
分析上述代码可以知道
当多个线程访问SychronizedDemo的run方法时, 以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的, 一个线程想要执行synchorized修饰的方法里的代码,首先尝试获得锁:
- 如果拿到锁。执行synchronized代码体的内容
- 如果拿不到锁。这个线程就会不断地尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁(也就是会有锁竞争的问题)
2.3多个线程多把锁
多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。
/**
*当是非静态时有两把锁,如果静态时只有一把锁
*/
public class MultThreadMultLock{
private /*static*/ int num = 5;
public /*static*/ synchronized void fun(String tag){
try {
if ("a".equals(tag)){
num = 100;
System.out.println("tag = a");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("tag = b");
}
System.out.println("tag="+tag+",num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final MultThreadMultLock m1 = new MultThreadMultLock();
final MultThreadMultLock m2 = new MultThreadMultLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.fun("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.fun("b");
}
});
t1.start();
t2.start();
}
}
没有static运行结果:
有static运行结果:
总结分析上述运行结果可发现:
1.关键字synchronized取得的锁都是对象锁 而不是把一 段代码(或方法)当做锁。
所以Demo中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lcck))。
两个对象,线程获得的就是两个不同的锁,它们之间互不影响
2.另一种情况是相同的锁,即在静态方法上加synchronized关键字, 表示锁定class类, 类级别的锁(独占.class类)
2.1对象锁的同步和异步
2.1.1同步-synchronized
同步就是共享,如果不是共享的资源,就没有必要进行同步。
2.1.2异步-asynchronized
异步就是独立,相互之间不受到任何制约。
2.1.3同步与线程安全
同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性:
●原子性(同步)
●可见性
/**
*对象锁的同步与异步
*/
public class synchronizedDemo{
public synchronized void fun1(){
try {
System.out.println("fun11111");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public /*synchronized*/ void fun2(){
System.out.println("fun22222");
}
public static void main(String[] args) {
final synchronizedDemo synchronizedDemo1 = new synchronizedDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo1.fun1();
}
},"thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo1.fun2();
}
},"thread2");
thread1.start();
thread2.start();
}
}
分析总结:
●线程1先持有Object对象的Lock锁, 线程2如果在这个时候调用对象中的同步(synchronized)方法, 则需等待,也就是同步
●线程1先持有Object对象的Lock锁,线程2可以以异步的方式调用对象中的非synchronized修饰的方法
2.2脏读
什么是脏读?
对于对象的同步和异步的方法,我们在设计的时候,一定要考虑问题的整体,不然就会出现数据不一致的错误, 很经典的错误就是脏读----dirty read
/**
*
*/
public class DirtyReadDemo{
private String name = "张三";
private String pwd = "123456";
public synchronized void setValue(String name,String pwd){
this.name = name;
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.pwd = pwd;
System.out.println(Thread.currentThread().getName()+" setValue方法中:name = " + name +" pwd = "+pwd);
}
//public void getValue(){
public synchronized void getValue(){
System.out.println(Thread.currentThread().getName()+" getValue方法中:name = " + name +" pwd = "+pwd);
}
public static void main(String[] args) {
final DirtyReadDemo dirtyReadDemo = new DirtyReadDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
dirtyReadDemo.setValue("李四","666666555555544444");
}
},"thread1");
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
dirtyReadDemo.getValue();
}
}
分析总结:
●在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/ getValue方法同时加锁synchronized同步关键字,保证业务的原子性,不然会出现业务错误(也从侧面保证业务的一致性)
2.3 synchronized其他概念
1.锁重入
关键字synchronized拥有锁重入的功能
也就是在使用synchronized时, 当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
如果遇到异常,锁将自动被释放。
锁重入第一个demo,同一个对象的不同方法
public class SynchronizedReDemo1 {
public void fun1(){
System.out.println("fun1------");
fun2();
}
public void fun2(){
System.out.println("fun2------");
fun3();
}
public void fun3(){
System.out.println("fun3------");
}
public static void main(String[] args) {
final SynchronizedReDemo1 synchronizedReDemo1 = new SynchronizedReDemo1();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronizedReDemo1.fun1();
}
});
thread.start();
}
}
那么锁重入在子父类之间呢??当然也是可以的
public class SynchronizedReDemo2 {
static class Fu{
public int i = 5;
public synchronized void fuFun(){
i--;
System.out.println("fu 打印i = "+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Zi extends Fu{
public synchronized void ziFun(){
while (i > 0){
try {
i--;
System.out.println("子类打印i = "+i);
Thread.sleep(1000);
this.fuFun();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Zi zi = new Zi();
zi.ziFun();
}
});
thread.start();
}
}
如果有异常!!!!????
public class synOper {
private int i = 0;
public synchronized void oper(){
try {
while (true){
i++;
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" i="+i);
if (i==10){
Integer.parseInt("a");
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("异常!i="+i);
}
}
public static void main(String[] args) {
final synOper synOper = new synOper();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synOper.oper();
}
},"thread1");
thread.start();
}
}
对有异常分析总结:
●对于Web应用程序,异常释放锁的情况,如果不及时处理,很可能对应用程序业务逻辑产生严重的错误。
比如现在正在执行一一个队列任务,很多对象都去在等待第一个对象正确执行完毕再去释放锁,但是第一个对象由于异常的出现,导致业务逻辑没有正常执行完毕,就释放了锁,那么可想而知后续的对象执行的都是错误的逻辑。
2.4 synchronized代码块
必须要有两个或两个以上的线程,需要发生同步
必须是多线程使用同一个锁
必须保证同步中只有一个线程在运行
好处:解决了多线程的安全问题
弊端:效率低
比如A线程调用同步的方法执行一个很长时间的任务
那么B线程就必须等待比较长的时间才能执行
这样的情况下,可以使用synchronized 代码块本优化代码执行时间,也就是通常所说的减小锁的粒度。
1.synchronized可 以使用任意的Object进行加锁,用法比较灵活
Object lock = new Object();
public void fun() {
synchronized (lock) {
try {
while (true){
System.out.println(Thread.currentThread().getName()+"开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.不要使用String的常量 加锁,会出现死循环问题
/**
*不能使用字符串常量加锁
*/
public class StringLock {
public void fun() {
synchronized (new String("字符串常量")) {
try {
while (true){
System.out.println(Thread.currentThread().getName()+"开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
StringLock stringLock = new StringLock();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
stringLock.fun();
}
},"thread");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.fun();
}
},"thread2");
thread.start();
thread2.start();
}
}
3.锁对象的改变问题,当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同
●如果对象本身不发生改变,那么依然还是同步的,即使对象的属性已经发生了改变
死锁问题
在设计程序时就应该避免双方相互持有对方的锁的情况,在互相不释放的情况下,会出现死锁问题
public class DeadLock {
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag){
this.tag=tag;
}
public void run(){
synchronized (lock1){
if ("a".equals(tag)){
System.out.println(Thread.currentThread().getName()+"锁住lock1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"锁住lock2");
}
System.out.println("开始");
}
synchronized (lock2){
if ("b".equals(tag)){
System.out.println(Thread.currentThread().getName()+"锁住lock2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+"锁住lock1");
}
System.out.println("开始");
}
}
}
}
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
deadLock1.setTag("a");
}
},"thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
deadLock2.setTag("b");
}
},"thread1");
thread1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
java内存模型
2.5volatile关键字
概念
volatile关键字的主要作用是使变量在多个线程间可见。可以避免脏读
强制线程到主内存(共享内存)里去读取变量,
而不去线程工作内存区里去读取,
从而实现了多个线程间的变量可见。
也就是满足线程安全的可见性。
先来个例子
/**
*volatile 保证线程安全的可见性
*/
public class valDemo extends Thread{
private volatile boolean isRunning = true;
public void setRunning(Boolean isRunning){
this.isRunning = isRunning;
}
public void run(){
System.out.println("开始了");
while (isRunning == true){
//
}
System.out.println("结束了");
}
public static void main(String[] args) throws InterruptedException{
valDemo valDemo = new valDemo();
valDemo.start();
Thread.sleep(3000);
valDemo.setRunning(false);
System.out.println("已设置false");
Thread.sleep(1000);
System.out.println(valDemo.isRunning);
}
}
不加volatile关键字
加volatile关键字
1.在上述例子中可以发现:在Java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝。
当线程执行时,它在自己的工作内存中操作这些变量。
为了存取一个共享的变量, 一个线程通常先获取锁,并去清除它的内存工作区,把这些共享变量从所有线程的共享内存中正确地装入到他自己所在的工作内存区中,当线程解锁时保证该工作内存区中变量的值写回到共享内存中
一个线程可以执行的操作有使用、赋值、装载、存储、锁定、解锁,而主内存可以执行的操作有读、写、锁定、解锁,每个操作都是原子的
再来个例子:
/**
* volatile不能保证原子性
*/
public class VolNoAtomic extends Thread{
private static volatile int count;
private static volatile int a = 0;
private synchronized static void addCount(){
for (int i = 0; i < 100; i++) {
count++;
}
a++;
System.out.println("count"+ a +" = "+ count);
}
public void run() {
addCount();
}
public static void main(String[] args) {
VolNoAtomic[] volNoAtomic = new VolNoAtomic[10];
for (int i = 0; i < 10; i++){
volNoAtomic[i] = new VolNoAtomic();
}
for (int i = 0; i < 10; i++){
volNoAtomic[i].start();
}
}
}
2.这个例子说明:
volatile关键字 虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上一个轻量级的synchronized ,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里面,比如Netty的底层代码就大量使用volatile,可见Netty性能一定是非常不错的。 )
volatile 一般用于针对多个线程可见的变量操作,并不能代替synchronized的同步功能
第三个例子:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
*atomic类只保证本身方法原子性
* 并不保证多次操作的原子性,如果想保证的话就结合synchronized使用
*/
public class AtoNo extends Thread {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void run() {
add();
}
public /*synchronized */int add() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.addAndGet(1);
atomicInteger.addAndGet(2);
atomicInteger.addAndGet(3);
atomicInteger.addAndGet(4);
System.out.println(Thread.currentThread().getName()+" "+atomicInteger.get());
return atomicInteger.get();
}
public static void main(String[] args) {
List<Thread> thread = new ArrayList<>();
AtoNo atoNo = new AtoNo();
for (int i = 0; i < 5; i++) {
thread.add(new Thread(atoNo,"thread"+i));
}
for (int i = 0; i < 5; i++) {
thread.get(i).start();
}
}
}
不加synchronized
加synchronized
3.上述例子说明volatile关键字只具有可见性,没有原子性。
要实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性,如果想要满足多次操作原子性,就结合关键字synchronized使用)
2.6线程之间通信
2.6.1概念
线程是操作系统中独立的个体,但这些个体不经过特殊处理就不能成为一个整体,线程间的通信就是成为整体的必用方式之一。
当线程存在通信指挥,系统间的交互性会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控与监督。
例如:使用volatile关键字实现两个线程之间的通信
package cx;
import java.util.ArrayList;
import java.util.List;
/**
* volatile保证变量的可见性,实现线程之间的通信
* 线程2使用轮询的方式,查询共享list的大小
*/
public class waitNotifyDemo {
private volatile static List list = new ArrayList();
public void add() {
list.add("cxcx");
}
public int size() {
return list.size();
}
public static void main(String[] args) {
final waitNotifyDemo waitNotifyDemo = new waitNotifyDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
waitNotifyDemo.add();
System.out.println(Thread.currentThread().getName() + "有元素了");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
//一直监控size的值
while (true){
if (waitNotifyDemo.size() == 5) {
System.out.println(Thread.currentThread().getName() + "五个元素了要抛异常了");
throw new RuntimeException();
}
}
}
});
thread1.start();
thread2.start();
}
}
2.6.2 wait和notify
使用wait和notify方法实现线程间的通信(注意这两个方法都是Object的方法,也就是说Java为所有的对象都提供了这两个方法)
- wait和notify必须配合synchronized关键字使用
- wait释放锁,notify不释放锁
package cx;
import java.util.ArrayList;
import java.util.List;
/**
* 使用wait和notify并且配合synchronized关键字来实现线程之间通讯,保证线程安全。
* 注意,wait不会释放锁,所以线程2要等线程1执行完才会获得锁
*/
public class waitNotifyDemo3 {
private volatile static List list = new ArrayList();
public void add() {
list.add("cxcx");
}
public int size() {
return list.size();
}
public static void main(String[] args) {
final waitNotifyDemo3 waitNotifyDemo = new waitNotifyDemo3();
final Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock){
for (int i = 0; i < 10; i++) {
waitNotifyDemo.add();
System.out.println(Thread.currentThread().getName() + "添加元素了!!");
Thread.sleep(500);
if (waitNotifyDemo.size() == 5){
System.out.println(Thread.currentThread().getName()+"已经达到五个元素了");
lock.notify();//不释放锁
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock){
if (waitNotifyDemo.size() != 5){
System.out.println(Thread.currentThread().getName()+"进来了");
lock.wait();//释放锁,线程2阻塞,此时运行线程1
System.out.println(Thread.currentThread().getName()+"出去了");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread2.start();
thread1.start();
}
}
没有达到实时获取通知的效果,使用并发包下的CountDownLatch可以实时发出通知
2.6.4ThreadLocal
概念:线程局部变量,是一种多线程间并发访问变量的解决方案。
与synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程的安全。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
从性能上来说,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程安全解决方案,在高并发量或者竞争激烈的场景,使用ThreadLocal可以在一定性能上减少竞争。
在开发项目的时候,登录时,将当前用户的信息放到ThreadLocal中,后续通过ThreadLocal去拿到
package cx;
/**
* 线程1在自己的线程副本中设置了值“cx”,
* 但是线程2中并没有给线程副本设置值。
*/
public class ThreadLocalDemo {
public static ThreadLocal<String> th = new ThreadLocal<>();
public void setTh(String name) {
th.set(name);
}
public void getTh() {
System.out.println(Thread.currentThread().getName() + ":" + this.th.get());
}
public static void main(String[] args) {
final ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
threadLocalDemo.setTh("cx");
threadLocalDemo.getTh();
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalDemo.getTh();
}
});
thread.start();
thread1.start();
}
}
2.7单例&多线程
单例模式,最常见的就是懒汉模式和饥饿模式
前者直接实例化对象,后者在调用方法的时候才会实例化对象。
在多线程模式中,考虑到性能和安全问题,我们一般选用以下两种,在提高性能的同时,又保证了线程安全:
- double check instance
- static inner class
package cx;
//双重检查
public class DoubleCheck {
private static DoubleCheck singleton;
public static DoubleCheck getInstance(){
if (singleton == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (DoubleCheck.class){
if (singleton == null){
singleton = new DoubleCheck();
}
}
return singleton;
}
}
package cx;
public class StaticInnerClass {
/*
* 内部静态类
* */
private static class Singleton{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return Singleton.singleton;
}
}
3.1 同步类容器
3.1.1同步类容器vs线程安全
同步类容器都是线程安全的,但是某些场景下可能需要加锁来保护复合操作。
复合操作例如:迭代(反复访问元素、遍历完容器中所有的元素),跳转(根据指定的顺序找到当前元素的下一个元素),以及条件运算。
这些复合操作在多线程并发地修改容器的时候,可能会表现意外的行为。最经典的就是ConcurrentModificationException,原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改问题
3.1.2同步类容器
如古老的Vector、HashTable。这些容器的同步功能其实都是由JDK的Collection.synchronized
等工厂方法去创建实现的。
其底层就是使用传统的synchronized关键字对每个公用的方法进行同步,使得每次只能又有一个线程访问容器的状态。
但是在现在,明显不满足互联网高并发的请求,虽然保证了线程安全,但是不具备足够好的性能
3.2并发类容器
3.2.1概念
JDK5.0之后提供了多种并发类容器来替代同步类容器从而改善性能。
同步类容器的状态都是串行化的。
他们虽然是实现了线程安全,但是严重降低了并发性(每次只能一个线程访问),在多线程环境下,严重降低了应用程序的吞吐量。
并发类容器是专门针对并发设计的,使用ConcurrentHashMap来替代给予散列的传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持。
以及使用了CopyOnWriteArrayList代替Vector,并发地CopyOnWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue(高性能的队列)和LinkedBlockingQueue(以阻塞形式的队列),具体实现Queue还有很多,例如:ArrayBlockingQueue,priorityBlockingQueue,SynchronousQueue等。
3.3 ConcurrentMap
3.3.1 类继承关系
3.3.2 两个重要的实现类
- ConcurrentHashMap(HashTable):JDK8中的实现也是锁分离的思想,它把锁分的比segment(JDK1.5)更细一些,只要hash不冲突,就不会出现并发获得锁的情况。它首先使用无锁操作CAS插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。性能比segment分段锁又再次提升。
3.3 Copy-On-Write容器
概念
简称COW,是一种用于程序设计的优化策略。适合读多写少
JDK里的COW容器有两种
- CopyOnWriteArrayList
- CopyOnWriteArraySet
CopyOnWriter容器即写时复制容器。
通俗的理解就是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器copy,复制出一个新的容器,然后在新的容器里添加元素,添加完元素后,再将原容器的引用指向新的容器
这样做的好处是:我们可以对CopyOnWrite容器进行并发的1读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。