Java多线程知识点整合 - 1
2018-12-05 本文已影响0人
Earl_iu
视频:https://www.bilibili.com/video/av33688545/?p=21
源码:[https://github.com/EduMoral/edu/tree/master/concurrent/src/yxxy]
书:Java并发编程实战+深入理解Java虚拟机
(https://github.com/EduMoral/edu/tree/master/concurrent/src/yxxy)
-
概述:
- 进程 process 是正在进行的程序,是不执行程序操作的,他只是分配了该应用程序的内存空间。
- 线程 thread 是负责进程中内容执行的一个控制单元,也称为执行路径,一个进程中至少要有一个线程。
- 一个进程中可以有多个执行路径,那就是多线程。
- 开启多个线程,是为了同时运行多个代码,每个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
-
好处与弊端:
- 可以解决同时执行多个程序的问题,但是线程太多会降低效率。
-
其实应用程序的执行都是cpu在进行着快速的切换完成的,这个切换是随机的。
image.png
-
看是否有线程安全隐患的关键是:是否有共享数据
-
当线程在临时阻塞状态时,不能被冻结,因为被冻结就需要调用sleep,只有运行的时候才有资格调用。
-
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再启动。
-
创建线程的第一种方式:继承Thread类
-
创建线程的第二种方式:实现Runnable接口,实例化Thread类,调用线程对象的start方法开启线程 // Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。
-
构造函数:
- Thread(Runnable Target) 分配新的Thread对象。 Target是其run方法被调用的对象
-
在使用Runnable接口时,Thread类中也有run方法,为什么是运行Runnable的Run方法呢?
- Thread类实现了Runnable接口,但是并没有完全实现run方法,此方法是由Runnable子类完成的,想要继承Thread类,就必须覆写run方法
class Thread{
private Runnable r;
Thread(){
}
Thread(Runnable r){
}
public void run(){
if(r!=null){
r.run();
}
}
public void start(){
run();
}
}
- 实现Runnable接口对比继承Thread类有哪些好处?
- 一个类继承Thread类,不适合多个线程的资源共享,实现Runnable接口,可以方便实现资源共享
- 因为一个线程只能启动一次,通过Thread实现线程时,线程和线程所要执行的任务是捆绑在一起的,也就是的一个任务只能开启一个线程,也不能让两个线程共享彼此之间的资源
- 一个任务可以启动多个线程,通过Runnable方式实现的线程,实际上是开辟了一个线程,然后将任务传进去,由此线程执行。可以实例化多个Thread对象,将同一个任务传进去,也就可以多个线程他同时执行一个任务
- 避免了Java单继承的局限性
- 同步代码块和同步方法 synchronized
- 为了解决并发操作可能造成的异常,Java多线程支持引入同步监视器来解决这个问题,使用同步监视器的通用方法就是使用同步代码块
- 使用“加锁-修改-释放锁”的逻辑
- 给某个对象加锁,锁是堆内存中的对象,不是栈内存中的引用
- 相同于原子操作,不可分的,其他的线程不能打断运行的线程
-
不要以字符串常 量作为锁对象
- 由于字符串常量池的存在,有时会出现两个字符串引用是同一把锁的情况
- 有时会出现引用的类库也使用字符串作为锁,例如“Hello”,但是使用时并不知道,有可能使用同一把锁
-
同步不具有继承性
- 如果父类的方法带synchronized关键字,子类继承并重写此方法的时候,并不继承synchronized
- 如果子类没有重写synchronized方法,那么子类使用该方法时依然是同步的
-
synchronized使用的四种方法:
- 给某个对象加锁,锁是堆内存中的对象,不是栈内存中的引用
- 相同于原子操作,不可分的,其他的线程不能打断运行的线程
public void n(){
synchronized(this){ // 自身为锁
count--;
}
}
Object o = new Object();
public void n(){
synchronized(o){
count--;
}
}
public synchronized void m(){ // 此方法等同于synchronized(this)
count--;
}
public synchronized static void m(){ // 当synchronizedy应用于静态方法时,等同于synchronized(T.class),以该类为锁
count--;
}
-
同步方法和非同步方法是否可以同时调用?
- 同步方法即synchronized方法
- 可以同时调用,只有同步方法在运行的时候才需要申请该同步方法的锁
-
对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题(dirtyRead)
- 脏读就是读取的时候,读到了写入还没有完成时的数据
// 当我们set了一个值以后,我们在调用getBalance的时候,很有可能set方法还没有完成,所以读写方法都需要同步
// getBalance方法同步以后,在set方法没结束以前不可以进行getBalance方法
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public synchronized double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
-
一个同步方法是否可以调用另外一个同步方法?
- 可以,一个线程已经拥有某个对象的锁,再次申请时的时候仍会得到该对象的锁
- synchronized获取的锁是可以重入的
- 重入锁就是同一个线程,同一把锁
synchronized void m1(){ // 当一个线程调用了m1以后,获得了this这把锁,但是m1调用了m2时,又需要申请获得相同的this锁,这种操作是可行的,因为在同一个线程以内
m2;
}
synchronized void m2(){
System.out.println();
}
-
子类中调用父类方法时锁是谁的?
- 锁是子类的,重入锁的另外一种情况
- 继承中super的理解,推荐看 https://blog.csdn.net/sujudz/article/details/8034770
public class inherit {
public static void main(String[] args) {
Son s = new Son();
s.doSomething();
}
}
class Father{
public synchronized void doSomething(){
System.out.println(this.getClass());
}
}
class Son extends Father{
public synchronized void doSomething(){
super.doSomething(); // 通过super引用调用从父类继承来的doSomething()方法,那么锁还是当前子类对象,super等同于(Father)this,本质上还是this对象
System.out.println(this.getClass());
}
}
output:
class multi.thread.Son
class multi.thread.Son
-
死锁的模拟
- 线程1调用a的时候,获取了锁lock_A
- 线程2调用b的时候,获取了锁lock_B
- 线程1在a方法内想调用b,但是锁已经被线程2拿走了
- 线程2在b方法内想调用a,但是锁已经被线程1拿走了
public void a(){
synchronized(lock_A){
b();
}
}
public void b(){
synchronized(lock_B){
a();
}
}
-
程序在执行过程中,如果出现异常,锁是会被默认释放的
- 如果不想释放锁,通过try catch捕获异常并处理
- 在并发处理中,有异常一定要多加小心
- 当一个线程运行到一半的时候,出现了异常,释放了锁,其他线程获取了该锁,可能会得到异常的值
-
volatile关键字
- 使一个变量在多个线程间可见
- cpu在运行线程的时候,都会有缓冲区,将数据读取到缓冲区中
- A,B线程都用到了一个变量,java默认是A线程保留了一份copy,这样B线程修改了该变量以后,线程A未必知道
- 使用volatile关键字,当变量的值改变时,会通知所有线程变量已经改变了
- 内存可见性,禁止重排序
- 线程的变量都会从工作内存中拿,而不是主内存,这样就会导致当一个线程修改某些变量的时候其他变量不知道
- volatile为什么能保证读到的值就一定是最新的呢?
- java语言中的特性,happens-before(先行发生原则)
- 如果一个事件发生在另一个事件之前,结果必须反应,即使这些事件实际上是乱序执行的
- 一个变量的写操作先行发生于一个变量的读操作,那么写操作一定在读操作之前完成
- 当他修改了主内存中的值以后,别的线程的副本将会无效,这样只能去主内存中读
- volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能代替synchronized
- volatile只保证可见性,不保证原子性,一旦一个共享变量(类的成员变量,类的静态成员变量)volatile修饰以后,它只要被修改,对所有线程来说都是立刻可见(即修改值以后会立刻写入主存)
- 当需要使用被volatile修饰的变量时,线程会从主内存中重新获取该变量的值,但当该线程修改完该变量的值写入主内存的时候,并没有判断主内存内该变量是否已经变化,故可能出现非预期的结果。
- 多线程访问volatile时不会发生阻塞,而synchronized关键字可能会发生阻塞
public class volatile_test implements Runnable{
volatile boolean running = true;
void m(){
System.out.println("start");
while(running){
}
System.out.println("end");
}
public void run() {
this.m();
}
public static void main(String[] args) {
volatile_test t = new volatile_test();
Thread th = new Thread(t);
th.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false; // 当没有volatile关键字的时候,running值的改变,线程并不会知道
}
}
-
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能代替synchronized
- volatile只保证可见性,不保证原子性
-
使用Atomicxxx类也可以解决原子性和可见性的问题
- Atomic类比synchronized更高效
- 如果只是对数字进行操作,可以使用AtomicInteger
- count.incrementAndGet() 用来替代 count++
- 但原子类不保证多个方法连续调用是原子性的,两个Atomic类的方法之间也可能出现被别的线程打断
if(count.get()<100){
count.incrementAndGet(); // 当count=999时,调用count.get()<1000,返回true以后,新的线程进入,将count+1,再调用count.incrementAndGet时,count已经等于1000了,结果会变成1001
}
-
锁定了对象o,如果o的属性发生变化,不影响锁的使用
- 但是如果o变成了另外一个对象,则锁定的对象发生改变,应该避免锁定对象的引用变成另外的对象
-
不要以字符串常量作为锁对象
- 下面的例子中m1和m2都是锁定了同一个对象
- 有时会出现引用的类库也使用字符串作为锁,例如“Hello”,但是使用时并不知道,有可能使用同一把锁
String s1 = "Hello";
String s2 = "Hello";
// 尽管引用不同,但是都指向了同一个String对象"Hello"
- 例题:实现一个容器,拥有get和add方法,开启两个线程,线程1给容器依次添加10个元素,线程2实现监控元素的个数,当个数到5时,线程2给出提示并结束
方法1:
// 让count参数变成volatile的,这样数据变化时会通知别的线程
public class count_control {
private ArrayList a = new ArrayList();
volatile int count = 0;
public void add(Object o){
a.add(o);
}
public int get(){
count = a.size();
return count;
}
public static void main(String[] args) {
count_control c = new count_control();
Thread t1 = new Thread(()->{
for(int i =0;i<10;i++){
c.add(new Object());
System.out.println(i);
}
});
t1.start();
new Thread(()->{
while(true){
if(c.get() ==5){ // 这里为Class t2的简写
break;
}
}
System.out.println("get 5");
});
t2 t3 = new t2(c);
t3.start();
}
}
class t2 extends Thread{
private count_control c;
t2(count_control c){
this.c = c;
}
public void run(){
while(true){
if(c.get() == 5){
break;
}
}
System.out.println("get2");
}
}
方法2: while循环的监控浪费cpu
使用wait和notify做到,wait会释放锁,而notify不会释放锁
public class count_cc2 {
private ArrayList a = new ArrayList();
int count = 0;
public void add(Object o){
a.add(o);
}
public int get(){
count = a.size();
return count;
}
public static void main(String[] args) {
final Object lock = new Object();
count_cc2 c2 = new count_cc2();
new Thread(()->{
synchronized(lock){
System.out.println("t2启动");
if(c2.get() != 5){
try {
lock.wait();
} catch (InterruptedException ex) {
Logger.getLogger(count_cc2.class.getName()).log(Level.SEVERE, null, ex);
}
}
System.out.println("t2关闭");
lock.notify();
}
}).start();
new Thread(()->{
System.out.println("t1启动");
synchronized(lock){
for(int i =0;i<10;i++){
c2.add(new Object());
System.out.println(i);
if(i ==5){
lock.notify();
try {
lock.wait();
} catch (InterruptedException ex) {
Logger.getLogger(count_cc2.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
},"t1").start();
}
}
方法3: 使用Latch 门闩代替wait notify
当不涉及同步,只涉及线程通信的时候,用synchronized和wait/notify显得太重
public class count_cc3 {
private ArrayList a = new ArrayList();
int count = 0;
public void add(Object o){
a.add(o);
}
public int get(){
count = a.size();
return count;
}
public static void main(String[] args) {
count_cc2 c2 = new count_cc2();
CountDownLatch cdl = new CountDownLatch(1);
new Thread(()->{
System.out.println("t2启动");
if(c2.get() != 5){
try {
cdl.await(); // 不为5的时候把latch关上,线程停止
} catch (InterruptedException ex) {
Logger.getLogger(count_cc3.class.getName()).log(Level.SEVERE, null, ex);
}
}
System.out.println("t2关闭");
}).start();
new Thread(()->{
System.out.println("t1启动");
for(int i =0;i<10;i++){
c2.add(new Object());
System.out.println(i);
if(c2.get() ==5){
cdl.countDown(); // 当为5的时候,我们把latch打开,线程1重新开启
}
}
}).start();
}
}
面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
能够支持2个生产者线程以及10个消费者线程的阻塞调用
使用wait和notify/notifyAll来实现
public class container<T> {
private LinkedList<T> link = new LinkedList<>();
private int Max = 10; // 最多10个元素
private int count = 0;
public synchronized T get(){
T t = null;
while(link.size() == 0){
try{
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
t = link.removeFirst();
count--;
this.notifyAll();
return t;
}
public synchronized void put(T t){
while(link.size() == Max){ // 在使用wait()的时候,大部分时间都是while而不是if,因为if判断时,wait结束以后会直接向下 进行,而不会再判断一次,如果这时候size又满了,就会出现异常
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
link.add(t);
count++;
this.notifyAll(); //通知消费者进行消费
}
public static void main(String args[]){
container<String> c = new container();
for(int i = 0;i<10;i++){
new Thread(()->System.out.println("获取:"+c.get())).start();
}
for(int i=0;i<2;i++){
new Thread(()->{
for(int j=0;j<25;j++){
c.put("11");
}
}).start();
}
}
}