zoukankan      html  css  js  c++  java
  • JAVA多线程

    synchronized 锁住当前对象的理解

    public class TT implements Runnable {
        int b = 100;
        
        public synchronized void m1() throws Exception{
            //Thread.sleep(2000);
            b = 1000;
            Thread.sleep(5000);
            System.out.println("b = " + b);
        }
        
        public  void m2() throws Exception {
            Thread.sleep(2000);
            b = 2000;
        }
        
        public void run() {
            try {
                m1();
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
        
        public static void main(String[] args) throws Exception {
            TT tt = new TT();
            Thread t = new Thread(tt);
            t.start();
            tt.m2();
        }
    }

    m1()方法和m2()方法都访问了成员变量b,m1有加锁(对调用m1方法的对象加锁) m2没有加锁

    打印结果:b=2000 tt.m2()方法并没有等t线程执行结束就访问了m1()  t.start()和tt.m2()并没有同步

    结论:

    类的这个对象,是一个资源
    这个资源能不能好好的被访问,就像账户里的钱 能不能保证前后访问一致
    你必须把访问这个资源的所有的方法,都要考虑到是否设置成同步的(加锁)

    锁住当前对象只是针对加了synchronized 关键字m1()这段方法,另外一个线程绝对不可能执行这段代码,但是有可能执行其他的代码m2()

    模拟生产者与消费者

    生产者和消费者例子中,蕴含了多线程很多个知识点

    启动线程、 synchronized关键字sleep 、 wait 和notify的用法 以及数据结构中的栈

    package javaee.net.cn.thread;
    public class ProducerConsumer {
        //这里用是三个生产者线程和一个消费者线程来测试
        //如果想要把生产者生产的全部消费完 消费者的run方法执行的次数就需要时生产者的三倍 (i<6)
        public static void main(String[] args) {
            SyncStack ss = new SyncStack();
            Producer p = new Producer(ss);
            Consumer c = new Consumer(ss);
            //启动线程是调用start()方法,如果直接调用run方法就是普通的方法调用,不是异步的。
            new Thread(p).start();
            new Thread(p).start();
            new Thread(p).start();
            new Thread(c).start();
        }
    }
    //这是声明一个对象 用来放进栈里面
    class WoTou {
        int id; 
        WoTou(int id) {
            this.id = id;
        }
        public String toString() {
            return "WoTou : " + id;
        }
    }
    /**
     * 数据结构中 栈
     * 有push和pop方法 
     */
    class SyncStack {
        int index = 0;
        WoTou[] arrWT = new WoTou[6];
        //因为arrWT 和index是成员变量,所以需要对这两个方法加锁。 加锁是锁住当前对象,他里面的成员变量自然也就锁定了
        public synchronized void push(WoTou wt) {
            //须用while 而不是if 因为如果栈满了 需要一直等待
            while(index == arrWT.length) { 
                try {
                    //栈满了时 需要停止住
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //wait不同于sleep 不需要指定睡眠的时间, 但需要手动唤醒. 当index不等arrWT.length 表示栈现在不是满的可以进行push 于是手动唤醒线程
            this.notifyAll();    
            //如果不加锁 arrWT和index因为多线程同步 导致新加的窝头和指针不匹配
            //先把当前指针 执行新加的窝头
            arrWT[index] = wt;
            //然后把指针加1
            index ++; 
        }
        //pop操作和push操作刚好相反
        public synchronized WoTou pop() {
            while(index == 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.notifyAll();
            index--;
            return arrWT[index];
        }
    }
    /**
     * 定义一个生产者 每生产一个对象 睡眠200ms
     */
    class Producer implements Runnable {
        SyncStack ss = null;
        Producer(SyncStack ss) {
            this.ss = ss;
        }
        @Override
        public void run() {
            for(int i=0; i<2; i++) {
                WoTou wt = new WoTou(i);
                ss.push(wt);
                System.out.println("生产了:" + wt);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }            
            }
        }
    }
    /**
     * 定义一个消费者 
     */
    class Consumer implements Runnable {
        SyncStack ss = null;
        Consumer(SyncStack ss) {
            this.ss = ss;
        }
        @Override
        public void run() {
            for(int i=0; i<6; i++) {
                WoTou wt = ss.pop();
                System.out.println("消费了: " + wt);
                try {
                    //每消费一个对象 睡眠200ms  可以在console控制台 慢慢的看到线程的调度执行
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }            
            }
        }
    }
    View Code

    生产了:WoTou : 0
    生产了:WoTou : 0
    生产了:WoTou : 0
    生产了:WoTou : 1
    消费了: WoTou : 1
    消费了: WoTou : 1
    消费了: WoTou : 1
    生产了:WoTou : 1
    生产了:WoTou : 1
    消费了: WoTou : 0
    消费了: WoTou : 0
    消费了: WoTou : 0

    这是三个生产者一个消费者 最后的打印结果,可见 生产的窝头全部都被消费了

    补充 volatile关键字和syschronized

    synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中的这个方法不再保持同步,除非也用synchronized修饰(锁重入)

    synchonized 最能用来锁定字符串常量(因为常量池里面只有一个对象 如果允许,各种第三方jar包都锁定了同一个对象),也不能锁定基础的数据类型

    补充:程序之中如果出现了异常,锁会被释放,会被其他程序冲进来(程序乱入,导致数据不一致 访问到异常产生中间的数据)

    解决办法:Catch住异常

    线程的常用方法和关键字

    wait()和join()方法都会释放锁(join方法的底层也是使用了wait方法)

    wait:的目的是为了多个线程抢资源的时候,当前线程把资源让出来
    如果没有锁,就不存在两个线程抢资源的情况,也就没有必要把资源让出来,两个线程早就一起跑了 这也很好解释了为什么wait方法要和synchronized关键字连用。

    join():合并某个线程,他调用此方法的线程(别的线程)合并到当前线程上来。等待调用此方法的线程执行完了,当前线程才开始继续执行(经常用于等待另外一个线程的结束,有点像方法调用)。

    interrupt():中断阻塞会释放当前锁(如执行System.in.read() 也会进入阻塞状态等待用户的输入、调用了其他线程的join()方法、等待获取某个对象的锁 如死锁、wait())等都会进入阻塞状态。

    注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的

    sleep()和yield()方法,当前线程都会放弃CPU,并且都不会释放锁

    sleep():的过程之中 如果对当前对象加锁了(可以不加),不会释放当前对象的锁。

    yield(): 当前线程会做出让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示

    ----------------------------------------------------------------------------------------------

    notify():当一个线程执行了s.notify()后,如果在对象s上等待有许多个线程,那么java虚拟机随机取出一个线程,把它放到对象s的锁池去竞争获取锁的机会;

    如果对象s的锁池中没有任何线程,那么notify()方法什么也不做。

    等待池:假如线程A调用了某个对下的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该线程的等待池中,进入到等待池中的线程不会竞争到该对象的锁。

    锁池:   假如线程A已经拥有了某个对下的锁,而其他线程B,C想要调用这个对下的某个Synchronized方法,

            此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池

    JAVA多线程死锁

    产生死锁的原因:

    不同的线程等待不可能被释放的锁 互相等待对方释放锁

    看一个死锁的例子,

    /**
     * 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步
     * 或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的
     */
    public class TestDeadLock implements Runnable {
        public int flag = 1;
        private static Object o1 = new Object();
        private static Object o2 = new Object();
        public void run() {
            System.out.println("flag=" + flag);
            if(flag == 1) {
                synchronized(o1) {
                    try {
                        //睡眠500ms,让t2线程执行先锁住o2
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //因为t1线程睡眠了500ms t1执行到这里的时,o2已经被 t2锁住了
                    synchronized(o2) {
                        System.out.println("死锁住了1");    
                    }
                }
            }
            if(flag == 2) {
                synchronized(o2) {
                    synchronized(o1) {
                        System.out.println("死锁住了2");
                    }
                }
            }
        }    
        
        public static void main(String[] args) {
            TestDeadLock td1 = new TestDeadLock();
            TestDeadLock td2 = new TestDeadLock();
            td1.flag = 1;
            td2.flag = 2;
            Thread t1 = new Thread(td1);
            Thread t2 = new Thread(td2);
            t1.start();
            t2.start();        
        }
    }

    上面代码执行的流程是,线程t1和线程t2异步执行。 执行线程t1的时 先锁住o1 此时线程t1 睡眠500ms。t2线程开始执行,t2线程锁住o2

    等t2线程执行的时候 需要拿到o1的锁才能执行下去,此时1线程往下执行的时候 需要拿到o2的锁才能继续执行

    t1线程和t2线程互相持有对方锁(t1锁住了o1 拿到o2的锁就可以继续执行了,t2锁住了o2,拿到o1的锁就可以继续执行了) 陷入了死锁的状态。

    死锁解决办法
    死锁是程序设计的Bug 在设计程序时就要避免双方互相持有对方锁的情况,只要互相等待对方释放锁就有可能出现死锁

    当几个线程都需要访问共享资源A B和C时,保证每个线程都以相同的顺序执行他们,比如都先访问A,在访问B和C。

    concurrent并发包

    Lock

    ReentrantLocak和sysnchronized的区别

    支持异步的Callable接口和Future接口

    1)Callable接口:它和Runnable接口有点类似,都指定了线程所需要执行的操作。区别在于,Callable接口是在call()方法中指定线程所要执行的操作的,并且该方法有泛型的返回值<V>。

    此外 Callable实例不能像Runnable实例那样,直接作为Thread类的构造方法的参数。

    2)Future接口:能够保存异步运算的结果。

    get():方法返回异步运算的结果。如果运算结果还没处理,当前线程就会被阻塞,直到获得运算结果,才结束阻塞。

    下面程序演示两个线程之间异步运算的过程。

    public class Machine implements Callable<Integer>{
    
        @Override
        public Integer call() throws Exception {
            int sum=0;
            for(int a=0;a<100;a++){//计算从1加到100
                sum=sum+a;
                Thread.sleep(20);
            }
            return sum;
        }
    
        public static void main(String[] args) throws Exception {
            FutureTask<Integer> task=new FutureTask<Integer>(new Machine());
            Thread threadMachine = new Thread(task);
            threadMachine.start();// 执行Machine的call()方法
            System.out.println("等待计算结果...");
            //主线程调用task.get()方法 获得运算结果
            System.out.println("从1加到100的和:"+task.get());
            System.out.println("计算完毕");
        }
        
    }

    在以上程序中,Machine类实现了Callable接口,threadMachine线程负责执行Machine类的call()方法,该方法会计算从1加到100的和,并且返回运算的结果。

    主线程调用task.get()方法,当ThreadMachine线程还没运算完毕时,主线程就会阻塞,直到threadMachine线程执行完call()方法,主线程才会获得运算结果,并从task.get()方法中退出。

  • 相关阅读:
    【Spring Framework】10、代理模式
    【Spring Framework】8、使用注解开发
    Codeforces 516E
    AtCoder Grand Contest 055 题解
    Codeforces 1606F
    贪心/构造/DP 杂题选做
    整数拆分最大乘积
    CSP-S2021 被碾压记
    洛谷 P2791
    LCT 小记
  • 原文地址:https://www.cnblogs.com/ssskkk/p/9277588.html
Copyright © 2011-2022 走看看