美文网首页
Java多线程知识点整合 - 1

Java多线程知识点整合 - 1

作者: Earl_iu | 来源:发表于2018-12-05 01:53 被阅读0次

视频: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();
}
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关键字,当变量的值改变时,会通知所有线程变量已经改变了
    • 内存可见性,禁止重排序
    • 线程的变量都会从工作内存中拿,而不是主内存,这样就会导致当一个线程修改某些变量的时候其他变量不知道
java工作的内存模型
  • volatile为什么能保证读到的值就一定是最新的呢?
    • java语言中的特性,happens-before(先行发生原则)
    • 如果一个事件发生在另一个事件之前,结果必须反应,即使这些事件实际上是乱序执行的
    • 一个变量的写操作先行发生于一个变量的读操作,那么写操作一定在读操作之前完成
    • 当他修改了主内存中的值以后,别的线程的副本将会无效,这样只能去主内存中读
  • volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能代替synchronized
    • volatile只保证可见性,不保证原子性,一旦一个共享变量(类的成员变量,类的静态成员变量)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();
        }
    }
}

相关文章

网友评论

      本文标题:Java多线程知识点整合 - 1

      本文链接:https://www.haomeiwen.com/subject/gubbcqtx.html