title: 多线程
cover:
多线程
- synchronized关键字
- synchronized修饰静态方法以及同步代码块的synchronized(类.class)用法锁的是类,线程想要执行对应的同步代码,需要获得类锁
- synchronized修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁
- volatile关键字
- volatile关键字是用来保证有序性和关键性。
- volatile变量规则:
- 有序性:对一个变量的写操作先行发生于后面对这个变量的读操作;有序性实现的是通过插入内存屏障来保证的
- 可见性:java内存模型分为(主内存和工作内存)。比如线程A从主存把变量读到了自己的工作内存中,做了加1的操作,但是没有及时的将变量的值刷新,线程B读到的依旧是变量之前的值,加了volatile关键字的代码生成的汇编代码,会多出一个Lock前缀指令,Lock指令对Intel平台的cpu早期是锁总线,代价比较高,后面题出了缓存一致协议MESI,保证数据之间不一致问题
- Synchronized和Lock
synchronized是java的关键字,用来修饰方法和一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁,锁粗化,轻量级锁,偏向锁来优化关键字的性能。
Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;synchronized在发生异常时,会自动释放线程占有的锁,因而不会导致死锁现象发生;而Lock在发生异常时,若果没有主动通过unlock()方法去释放锁,很有可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待的锁的线程响应中断,而synchronized不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
- synchronized修饰方法和代码块的区别
修饰方法:一次只能有一个线程进入这个方法
修饰代码块:代码块只能写在方法内部,可以有多个线程进入方法,但是只能有一个线程执行这个代码块中的代码
进程
- 概念: 操作系统中并发执行的多个任务(只有正在运行的程序,才能称为进程)
线程
-
概念: 进程中的多个程序逻辑,则称为线程,也称为轻量级进程
-
特点: 线程是进程的基本组成单元
- java中只有多线程,没有多进程
-
线程的组成
- CPU时间片 由OS负责调度分配
- 数据
- 堆空间 堆空间由多个线程共享
- 栈空间 每个线程都有自己独立的栈空间
创建线程
- 实现Runnable接口,实现run()方法
- 继承Thread类,覆盖run()方法
线程的基本状态
- 图解
- 等待状态
- sleep(),让当前线程进入有限期等待状态,线程并没有放弃锁标记,进入就绪状态
- wait(),让当前线程进入无限期等待状态,线程放弃锁标记,等待被其他线程唤醒
- t.join(),让t线程加入当前线程,优先执行t线程,在执行当前线程,当t线程执行完后,再执行当前线程
线程池
-
Executor 线程池的总接口
- 所有线程池类与接口都在java.util.concurrent包下
-
ExecutorService Executor的子接口
- 通过submit()提交任务
- 通过shutdown(),关闭线程池
-
Executors 线程池工厂,提供线程池对象
- newFixedThreadPool(int n),创建线程池,里面有固定数量的线程吃对象,需要自己来传入参数
- newCachedThreadPool(),创建线程池,池中数量没有上线,需要几个就创建几个
Callable
-
类似于Runnable的接口,Callable实现类对象类似于Runnable实现类对象,是一个任务对象
- V call(); 类似于Runnable中的void run();
-
Future接口:获取线程的返回值或异常
-
V get(): 获取返回值 若线程尚未结束,获取不到值;则等待线程执行结束,再获取值
run()弊端:没有返回值、不能抛异常 call()优点:有返回值,可以抛任意异常
-
线程安全问题
-
线程不安全:当多个线程访问同一对象(临界资源),可能破坏了不可分割操作(原子操作),
导致数据不一致
-
临界资源:多个线程访问的同一对象
-
原子操作: 不可分割的操作,多个操作要么都执行成功,要么都执行失败
线程同步
-
同步代码块,只能写在方法内部
synchronized(Object o){ //原子操作 } 对o对象加锁的同步代码块
- Java中任何一个Object对象,都有一个互斥锁标记,用来分配给线程:只有具有锁标记的线程才能进入同步代码块中执行代码,代码执行完,线程会释放锁标记;没有拿到锁标记的线程,进入阻塞状态,直到拿到锁标记,在进入就绪状态,等待分配时间片执行同步代码块中的代码。
-
同步方法
访问修饰符 synchronized 返回值类型 方法名(形参列表){ //原子操作 } 等价于 访问修饰符 返回值类型 方法名(形参列表){ synchronized(this){ //原子操作 } }
- 同步方法对当前对象加锁,只有拿到当前对象锁标记的线程,才能执行同步方法
-
死锁
-
o.wait(),在当前线程调用o的wait方法,会使当前线程释放锁标记,自身线程进入o的等待队列,进入阻塞状态,等待被其他线程唤醒
-
o.notify()/o.notifyAll(),从o的等待队列中随机唤醒一个线程/所有线程,对此线程没有任何影响
-
必须用在对o枷锁的同步代码块中
面试问题:wait()与sleep()的区别 wait(): 无限期阻塞 会释放CPU时间片及锁标记,必须等待唤醒,才能再次争抢锁标记及CPU时 间片 sleep(): 有限期等待 只会释放CPU时间片,不会释放锁标记
-
如何避免临界资源数据不一致?
-
尽量避免使用临界资源对象,多使用局部变量(局部变量存在栈中,栈空间是每个线程独立的)
-
多使用线程安全并且可以高并发的对象
-
使用synchronized
-
-
线程安全的集合类以及队列
-
ConcurrentHashMap 线程安全并且可以高并发的集合
- JDK1.7 分段锁(16), 控制锁的粒度,将Map集合分成16段,每段都加一个不同锁
- JDK1.8 CAS算法(无锁算法)+synchronized
-
CopyOnWriteArrayList 应用于读操作远远多于写操作的场景下;牺牲写操作的效率,提升读操作的效率
-
读操作:获取集合中的元素 不加锁
-
写操作:改变集合中的元素 加锁,每次写操作都会复制出一个新集合,在新集合的基础上进行写操作
-
-
CopyOnWriteArraySet 同上
-
Queue 队列,FIFO(先进先出)
- 实现类
- ConcurrentLinkedQueue
- Queue的常用方法
- 添加
- add(E e) 当队列满了,报异常
- offer(E e) 当队列满了,不报异常,只返回false
- 获取
- element() 获取头元素,当获取不到,报异常
- peek() 获取头元素,当获取不到,返回null
- 删除
- remove() 删除并返回头元素,当队列为空,没有头元素,报异常
- poll() 删除并返回头元素,当队列为空,没有头元素,不报异常,返回null
- 添加
- BlockingQueue Queue的子接口 阻塞队列 当线程从队列中获取元素时,若队列为空,没有元素,会使获取元素的线程进入阻塞状态
- ArrayBlockingQueue 有界队列 当线程往队列中添加元素,若队列满了,添加不进去,会使添加元素的线程进入阻塞状态
- LinkedBlockingQueue 无界队列
- 实现类
线程安全之生产者消费者问题
-
生产和消费不能同时进行,放入或取出,会有一个信号发送给对方
//生产者消费者问题 public class TestProductConsumer { public static void main(String[] args) { MyStack stack = new MyStack(); //创建线程进行入栈操作 Runnable r1 = new Runnable() { public void run() { for(char c='A';c<='Z';c++ ) { stack.push(c+""); } } }; //创建线程进行出栈操作 Runnable r2 = new Runnable() { public void run() { for(int i = 0;i<26;i++) { stack.pop(); } } }; //启动线程 new Thread(r1).start(); new Thread(r1).start(); new Thread(r2).start(); new Thread(r2).start(); } } //数组实现栈 class MyStack{ private String[] str = {"","","","","",""}; private int index; //入栈 public synchronized void push(String s) { //当数组满的时候,让入栈的操作等待 //当有多个入栈线程时,采用while判断,不能用if while(index == str.length) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(s+" pushed "); str[index++] = s; print(); //当数组中有元素的时候,唤醒出栈操作 this.notifyAll(); } //出栈 public synchronized void pop() { //数组为空的时候,出栈的操作等待 //采用多个出栈线程时,不能用if while(index == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } index--; System.out.print(str[index]+" poped "); str[index] = ""; print(); //数组中无元素的时候,唤醒入栈操作 this.notifyAll(); } //打印栈中数据 public void print() { System.out.println(Arrays.toString(str)); } }
ndex–;
System.out.print(str[index]+" poped ");
str[index] = “”;
print();
//数组中无元素的时候,唤醒入栈操作
this.notifyAll();
}
//打印栈中数据
public void print() {
System.out.println(Arrays.toString(str));
}
}