多线程(二)

虽然我们可以理解同步代码块和同步方法锁对象的问题,但是我们没有直接看到在哪加了锁,在哪里释放了锁,为了更直观的加锁和释放锁,jdk5以后提供了一个新的锁对象Lock

之前那个卖票的例子,用Lock实现

public class MyRunable implements Runnable {
    int p = 100;
    private Lock lock=new ReentrantLock();
    
    @Override
    public void run() {
        while (true) {
            try {
                //加锁
                lock.lock();
                if (p > 0) {
                    try {
                        // 模拟延时
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    p--;
                    System.out.println("卖了" + (100 - p) + "张票,还剩" + p + "张" + "----" + Thread.currentThread().getName());
                }else {
                    break;
                }
            } finally {
                // 释放锁
                lock.unlock();
            }
            
        }
    }

同步的弊端:
A:效率低
B:如果产生了同步嵌套,容易产生死锁问题。

死锁问题:是指多个线程在执行过程中,因争夺资源而产品的互相等待的问题。

举个列子

public class MyThread extends Thread{
    boolean flag;
    public MyThread(boolean flag) {
        this.flag=flag;
    }
    @Override
    public void run() {
        
        if(flag) {
            synchronized (DieLock.obja) {
                System.out.println("if obja");
                synchronized (DieLock.objb) {
                    System.out.println("if objb");
                }
            }
        }else {
            synchronized (DieLock.objb) {
                System.out.println("if objb");
                synchronized (DieLock.obja) {
                    System.out.println("if obja");
                }
            }
        }
    }
    public static void main(String[] args) throws Exception {
        MyThread thread1=new MyThread(true);
        MyThread thread2=new MyThread(false);
        thread1.start();
        thread2.start();
    }
}

以上代码就会产生死锁问题。
解析:当thread1线程进入if 锁住了obja对象,而同时thread2也进入了else 锁住了objb对象。而thread1想要往下走必须等待objb的对象的释放,而thread2往下走也必须等待obja对象的释放。从而两个线程进入了互相等待锁释放,从而形成了死锁。

线程之间的通信问题。
1、同一个资源类
2、生产者(设置资源)
3、消费者(获取资源)

举个例子:
1、资源类Student
2、生产者SetThread对Student设置属性
3、消费者GetThread获取Student属性

public class Student {
    String name;
    int age;
}

public class SetThread implements Runnable {
    
    Student student;
    
    int x=0;
    
    public SetThread(Student student) {
        this.student=student;
    }
        
    @Override
    public void run() {
        while(true) {
            if(x%2==0) {
                student.name="杨过";
                student.age=18;
            }else {
                student.name="郭靖";
                student.age=30;
            }
            x++;
        }
    }

}


public class GetThread implements Runnable {
    
    Student student;
    
    public GetThread(Student student) {
        this.student=student;
    }
        
    @Override
    public void run() {
        while(true) {
            System.out.println(student.name+"-----"+student.age);
        }
    }
}


public class Demo {

    public static void main(String[] args) {
        Student student = new Student();
        SetThread set=new SetThread(student);
        GetThread get=new GetThread(student);
        
        Thread t1=new Thread(set);
        Thread t2=new Thread(get);
        t1.start();
        t2.start();
    }
}

上面的代码就是setThread根据x取模的值,不停的设置student的属性,而getThread就是不停的获取student的属性
运行发现出现了异常情况:


image.png

输出数据不但出现了重复,而且名字和年龄也匹配不上。

解析:出现了多次同样的数据,很好解释,因为getThread的抢到了多次执行权,所以还没来得级设置就打印了多次。
名字和年龄匹配不上是因为:setThread进入run方法,在设置名称完毕,刚准备设置年龄时就被getThread抢到了执行权,所以getThread获取到的是setThread设置完毕的名字和setThread还没来得及设置的年龄。所以就出现了年龄和名字不匹配。

修改代码:

    @Override
    public void run() {
        while(true) {
            synchronized (Student.class) {
                if(x%2==0) {
                    student.name="杨过";
                    student.age=18;
                }else {
                    student.name="郭靖";
                    student.age=30;
                }
                x++;
            }
        }
    }
    @Override
    public void run() {
        
            while(true) {
                synchronized (Student.class) {
                    System.out.println(student.name+"-----"+student.age);
                }
            }
        
    }

这里就加了锁,就不会出现名字和年龄不匹配了。注意这里也要对消费者加锁,而且要加同一锁。

之前我没对消费者(getThread)加锁,还是会出现年龄和名字不匹配的问题。还是对锁不太熟悉==!。
解析:如果只对生产者加了锁,这时你并没有把getThread加入到同步代码块中,它还是可以抢占setThread的cpu使用权,从而出现了年龄和名字不匹配的问题。如果对消费者加了锁,它就必须等待Student.class锁的释放(等待setThread的设置属性的代码执行完毕),才能打印值,这样就不会出现年龄和名字不匹配的问题了。

上面的代码还存在着一下问题:
1、如果第一个执行的是消费者,那么生产者还没有设置数据。消费者去获取数据显然也是无意义的,应该等待着生产者生产完数据再去获取数据。
2、如果第一次执行的是生产者,生产完数据后,他还拥有着执行权,继续生产数据,将之前的数据覆盖了,显然也是不合理的。应该等消费者消费完,然后再生产。

正常的思路:
A:生产者
看是否有数据,有就等待,没有就生产,生产完后通知消费者来消费数据。
B:消费者
先看是否有数据,有就消费,没有就等待,通知生产者生产数据。

为了解决这样的问题,java提供了一种机制:等待唤醒机制。

将代码改成如下:

public class Student {
    String name;
    int age;
    boolean flag;
}

public class SetThread implements Runnable {
    
    Student student;
    
    int x=0;
    
    public SetThread(Student student) {
        this.student=student;
    }
        
    @Override
    public void run() {
        while(true) {
            synchronized (student) {
                if(student.flag) {
                    try {
                        student.wait();//等待并释放锁。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(x%2==0) {
                    student.name="杨过";
                    student.age=18;
                }else {
                    student.name="郭靖";
                    student.age=30;
                }
                x++;
                student.flag=true;
                student.notify();//唤醒
            }
        }
    }

public class GetThread implements Runnable {
    
    Student student;
    
    public GetThread(Student student) {
        this.student=student;
    }
        
    @Override
    public void run() {
            while(true) {
                synchronized (student) {
                    if(!student.flag) {
                        try {
                            student.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(student.name+"-----"+student.age);
                    student.flag=false;
                    student.notify();//唤醒
                }
            }
        
    }
}

解析:假如消费者先抢到了线程,student.flag此时为false这样就会执行student.wait();线程等待释放锁,此时生产者就会获得Cpu执行权,然后设置值,并设置flag为true,执行student.notify();唤醒线程,然后生产者和消费者就会抢CPU的执行权,假设生产者又一次抢到了,那么执行student.wait();进入等待,消费者又会拿到执行权,并打印数据。

注意事项:
1、唤醒和等待一定要有锁对象的wait和notify方法,所以我将锁对象Student.class改成了student对象。
2、wait方法,除了让当前线程进入阻塞状态,还会释放锁。一旦被唤醒也是从wait所在的行往下执行,而不是从头开始执行。

image.png

线程组:java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,java允许程序直接对线程组控制。默认情况下所有的线程都属于同一个线程组。

1、获得线程组

        MyRunable runbable1 = new MyRunable();
        Thread thread1 = new Thread(runbable1,"线程1");
        Thread thread2 = new Thread(runbable1,"线程2");
        ThreadGroup group1=thread1.getThreadGroup();
        ThreadGroup group2=thread2.getThreadGroup();
        
        System.out.println(group1.getName()+"---"+group2.getName());

2、修改线程组

        ThreadGroup gp=new ThreadGroup("hello");
        MyRunable runbable1 = new MyRunable();
        Thread thread1 = new Thread(gp,runbable1,"线程1");
        Thread thread2 = new Thread(gp,runbable1,"线程2");
        ThreadGroup group1=thread1.getThreadGroup();
        ThreadGroup group2=thread2.getThreadGroup();
        System.out.println(group1.getName()+"---"+group2.getName());

3、可以通过线程组设置一些东西

gp.setDaemon(true);//通过线程组设置这个组的线程都是守护线程

线程池:程序启动一个新的线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中称为空闲状态,等待下一个对象来使用。

创建线程池,执行,并销毁。(Runable方式)

        //创建有2个线程的线程池
        ExecutorService pool=Executors.newFixedThreadPool(2);
        
        // 执行Runnable对象或者Callable对象
        pool.submit(new MyRunable());
        pool.submit(new MyRunable());
        pool.submit(new MyRunable());
        
        //结束线程池
        pool.shutdown();

创建线程池,执行,并销毁。(Callable方式)
需求线程一求0-99的和,线程二求0-199的和

public class MyCallable implements Callable {
    private Integer endNum;
    public MyCallable(Integer endNum) {
        this.endNum=endNum;
    }
    @Override
    public Integer call() throws Exception {
        int res=0;
        for (int i = 0; i < endNum; i++) {
            res+=i;
            System.out.println(res);
        }
        return res;
    }
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService pool=Executors.newFixedThreadPool(2);
        Future future1=pool.submit(new MyCallable(100));
        Future future2=pool.submit(new MyCallable(200));
        System.out.println(future1.get()+"----"+future2.get());
        
        pool.shutdown();
    }
}

匿名内部类启动多线程

Thread

        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(i);
                }
            }
        }.start();

Runnable

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(i);
                }
                
            }
        }).start();

面试题:

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("hello"+i);
                }
                
            }
        }) {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("world"+i);
                }
                
            }
        }.start();

这样会打印什么?
打印world,还是走的子类对象。

定时器:略(一般开发中用quarz做定时任务)

实现多线程有几种方式?
1、继承Thread类,重写run方法
2、实现runnable接口,重写run方法
3、实现callable接口,重写run方法,依赖于线程池使用

同步有几种方法
1、同步代码块
2、同步方法

启动一个线程调用run还是start,它们的区别?
调用start,调用run仅仅相当于调用一个普通方法,调用start会启动一个线程再调用run方法。

sleep和wait的区别
sleep必须指定睡眠多少秒,且不会释放锁,而wait可以不指定等待多少秒,且会释放锁。

为什么wait和notify,notifyAll是Object类的。
因为这些方法是依赖于锁对象的。

你可能感兴趣的:(多线程(二))