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

JAVA多线程

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

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()方法中退出。