欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

死磕java多线程

程序员文章站 2022-03-15 12:12:59
1.线程和进程 1.1线程和进程的区别 进程 它是内存中的一段独立的空间,可以负责当前应用程序的运行。当前这个进程负责调度当前程序中的所有运行细节(操作系统为进程分配一块独立的运行空间); 线程 它是位于进程中,负责当前进程中的某个具备独立运行资格的空间(进程为线程分配一块独立运行的空间); 进程是 ......

1.线程和进程

1.1线程和进程的区别

  • 进程 它是内存中的一段独立的空间,可以负责当前应用程序的运行。当前这个进程负责调度当前程序中的所有运行细节(操作系统为进程分配一块独立的运行空间);
  • 线程 它是位于进程中,负责当前进程中的某个具备独立运行资格的空间(进程为线程分配一块独立运行的空间); 进程是负责某个程序的执行,线程是负责进程中某个独立功能的运行,一个进程至少要包含一个线程。
  • 多线程 在一个进程中可以开启多个线程,让多个线程同时去完成某项任务。使用多线程的目的是为了提高程序的执行效率。

1.2线程运行状态

 

死磕java多线程

通过Thread类或Runnable接口创建线程对象之后进入初始状态;调用start方法进入可运行状态(就绪状态),此时并不是真正的运行,只是代表已经做好了运行前的各项装备;如果此线程获取到cpu的时间片,则进入到真正的可运行状态,执行run方法里面的业务逻辑;如果run方法执行完毕或调用stop方法则线程运行结束,进入死亡状态;在运行状态时调用不同方法也会进入其他不同状态,如果调用强制运行方法join或休眠方法将进入等待状态,时间到后自动进入就绪状态,随时准备获取cpu时间片;如果看到synchronized则进入同步队列等待状态,或者如果调用了wait方法则进入等待状态,等待状态的线程必须要通过notify唤醒才可进入等待状态,如果其它线程执行完毕,本线程拿到同步锁则进入就绪状态,等待获取cpu时间片。某个线程是否会执行只能看它能否争抢到cpu时间片,但是通过调高优先级来让线程更大概率的被优先执行。 

2.多线程

多线程运行的原理是:cpu在线程中做时间片的切换。cpu负责程序的执行,在每个时间点它其实只能运行一个程序而不是多个程序,不停的在多个程序之间高速切换,而一个程序其实就是一个进程即多个线程,说到底其实就是cpu在多个线程之间不停的做高速切换,而开多个线程就是不让cpu歇着,最大程度的压榨它来为程序服务。实现多线程有三种方式:继承Thread类;实现Runnable接口;使用线程池。

2.1继承Thread类

public class MyExtendsThread extends Thread {
    String flag;

    public MyExtendsThread(String flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        String name = Thread.currentThread().getName();
        System.out.println("线程"+name+"开始工作了...");
        Random random = new Random();
        for (int i = 0;i < 20;i++){
            try {
                Thread.sleep(random.nextInt(10)*100);
                System.out.println(name+"============="+flag);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread t0 = new MyExtendsThread("t0");
        Thread t1 = new MyExtendsThread("t1");

        t0.start();
        t1.start();
//        t0.run();
//        t1.run();
    }
}

调用线程要用start方法,而不是run方法,使用run方法只是调用方法,实际执行的还是Main线程,而调用start方法可以明显的看到线程争抢。

2.2实现Runnable接口

public class MyThreadImplementRunnable implements Runnable {

    int x;

    public MyThreadImplementRunnable(int x) {
        this.x = x;
    }
    
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("线程"+name+"开始执行");
        Random random = new Random();
        for(int i = 0;i<20;i++){
            try {
                Thread.sleep(random.nextInt(10)*100);
                System.out.println(name+"============="+x);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThreadImplementRunnable(1),"线程1");
        Thread t2 = new Thread(new MyThreadImplementRunnable(2),"线程2");
    
        t1.start();
        t2.start();
    }
}

2.3实现Callable接口

  • 创建实现Callable接口的类MyThreadImplementCallable;
  • 创建一个类对象:MyThreadImplementCallable callable = new MyThreadImplementCallable("测试");
  • 由Callable创建一个FutureTask对象: FutureTask futureTask = new FutureTask(callable); 注意:FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。
  • 由FutureTask创建一个Thread对象: Thread thread = new Thread(futureTask);
  • 启动线程: thread.start();
  • 获取任务线程执行结果 futureTask.get(); 注意:实现Callable接口的线程可以获得任务线程的执行结果;实现Runnable接口的线程无法获取任务线程执行的结果。
public class MyThreadImplementCallable implements Callable<String> {

    String name;
    public MyThreadImplementCallable(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"开始工作==============");
        Random random  = new Random();
        Thread.sleep(random.nextInt(5)*100);  //模拟执行业务
        return name+":执行完成";
    }

    public static void main(String[] args) throws Exception{
        MyThreadImplementCallable callable = new MyThreadImplementCallable("测试");
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        Thread thread = new Thread(futureTask);

        thread.start();
        String result = futureTask.get();  //获取任务线程执行结果
        System.out.println("线程的执行结果:"+result);
    }
}

2.4使用线程池

见下面的线程池专讲。 参考文档:Callable,Runnable比较及用法以及创建线程的4种方法

3.同步

3.1synchronized关键字

public class MySynchronized {
    public static void main(String[] args){
        final MySynchronized synchronized1 = new MySynchronized();
        final MySynchronized synchronized2 = new MySynchronized();
        new Thread("thread1"){
            @Override
            public void run(){
                synchronized (synchronized1){
                    try {
                        System.out.println(this.getName()+":start");
                        Thread.sleep(1000);
                        System.out.println(this.getName()+":wake up");
                        System.out.println(this.getName()+":end");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("thread2"){
            @Override
            public void run() {
                synchronized (synchronized1){  //争抢同一把锁时,线程1没释放之前,线程2只能等待
//                synchronized (synchronized2){ //如果不是一把锁,可以看到两句话交叉打印,发生争抢
                    System.out.println(this.getName()+":start");
                    System.out.println(this.getName()+":end");
                }
            }
        }.start();
    }
}

synchronized是java中的关键字,属于java语言的内置特性。如果一个代码块使用synchronized修饰,则这块代码是同步的,当一个线程获取到这个锁并且开始执行时,其它线程只能一直眼睁睁的等着这个线程执行然后释放锁,其中释放锁只有两种原因:1.线程正常执行完毕;2.线程执行时发生异常,jvm自动将锁释放。可以看到使用synchronized关键字之后每个时刻只会有一个线程执行代码块里面的共享代码,线程安全;缺点也很明显,其它线程只能等锁释放,资源浪费严重。

3.2Lock接口

  • lock和synchronized的区别: Lock不是Java语言内置的,它是一个接口,通过这个接口可以实现同步访问,synchronized是Java语言的关键字,是内置特性;Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。 Lock是一个接口,它里面有如下方法:
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的。 unLock()方法是用来释放锁的。

  • lock就是用来获取锁的,前面说到如果采用Lock,必须主动去释放锁。即使发生异常,程序也不会自动释放锁,因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
public class MyLock {

    private static ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private static Lock lock = new ReentrantLock();

    public static <E> void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();

                lock.lock();  //获取锁
                try {
                    System.out.println(thread.getName() + "得到了锁");
                    for (int i = 0; i < 5; i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                } finally {
                    System.out.println(thread.getName() + "释放了锁");
                    lock.unlock();  //释放锁
                }

            };
        }.start();

        new Thread() {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                lock.lock();
                try {
                    System.out.println(thread.getName() + "得到了锁");
                    for (int i = 0; i < 5; i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                } finally {
                    System.out.println(thread.getName() + "释放了锁");
                    lock.unlock();
                }

            };
        }.start();
    }
}
  • tryLock()表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直等待。
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法类似,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始就拿到锁或者在等待期间内拿到了锁,则返回true。
//观察现象:一个线程获得锁后,另一个线程取不到锁,不会一直等待
public class MyTryLock {

    private static List<Integer> arrayList = new ArrayList<Integer>();
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread("线程1") {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                boolean tryLock = lock.tryLock();
                System.out.println(thread.getName()+"======="+tryLock);
                if(tryLock){
                    try {
                        System.out.println(thread.getName() + "得到了锁");
                        for(int i = 0;i < 20;i++){
                            arrayList.add(i);
                        }
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                        System.out.println(thread.getName() + "释放了锁");
                    }
                }
            }
        }.start();

        new Thread("线程2") {
            @Override
            public void run() {
                Thread thread = Thread.currentThread();
                boolean tryLock = lock.tryLock();
                System.out.println(thread.getName()+"======="+tryLock);
                if(tryLock){
                    try {
                        System.out.println(thread.getName() + "得到了锁");
                        for(int i = 0;i < 20;i++){
                            arrayList.add(i);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                        System.out.println(thread.getName() + "释放了锁");
                    }
                }
            }
        }.start();
    }
}

线程1和线程2共享成员变量arrayList,当线程1获取锁的时候,线程2就获取不到锁,没办法执行它的业务逻辑,只有等线程1执行完毕,释放了锁,线程2才能获取锁,执行它的代码,进而保证了线程安全。

  • lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。 注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。 因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
  • Lock接口的实现类——ReentrantLock 直接使用lock接口的话,我们需要实现很多方法,不方便,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法,ReentrantLock,意思是“可重入锁”,使用它可以创建Lock对象。
  • ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

  • ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁,使用这个读写锁操作的结果就是:要么执行的全是读操作,结束完之后全执行写操作,中间不会交叉读写。
/**
 * @author 刘俊重
 * 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
 * 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
 */
public class MyReentrantReadWriteLock {

    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final MyReentrantReadWriteLock myTest = new MyReentrantReadWriteLock();
        new Thread("线程1"){
            @Override
            public void run(){
                myTest.read(Thread.currentThread());
                myTest.writer(Thread.currentThread());
            }
        }.start();

        new Thread("线程2"){
            @Override
            public void run(){
                myTest.read(Thread.currentThread());
                myTest.writer(Thread.currentThread());
            }
        }.start();
    }

    /**
     * @Description 读方法
     * @Author 刘俊重
     * @Date 2017/12/18
     */
    private void read(Thread thread){
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis()-start<=1){
                System.out.println(thread.getName()+"===正在执行读操作");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
            System.out.println(thread.getName()+"==释放读锁");
        }
    }

    /**
     * @Description 写方法
     * @Author 刘俊重
     * @Date 2017/12/18
     */
    private void writer(Thread thread){
        readWriteLock.writeLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis()-start<=1){
                System.out.println(thread.getName()+"===正在执行写操作");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
            System.out.println(thread.getName()+"==释放写锁");
        }
    }
}

Lock和Synchronized的选择:

  • Lock是一个接口,而sysnchrinized是java关键字,属于内置的语言实现;
  • synchronized关键字程序运行完成之后或出现异常时会释放锁,使用lock不会自动释放锁,只能自己使用unlock释放,否则会引起死锁,最好在finally中释放;
  • 使用lock可以使用trylock方法判断有没有获得锁,使用synchronized无法判断;
  • 使用lock可以让等待锁的线程中断,使用synchronized无法让线程中断,只能一直等待下去;
  • 使用lock可以提高多线程读操作的效率。 结论:如果竞争的资源不激烈,则使用synchronized和lock效率差不多;如果有大量线程同时竞争,则lock要远远优于synchronized。

4.volatile关键字

程序执行时有主内存,每个线程工作时也有自己的工作内存。当一个线程开始工作时会从主内存中拷贝一个变量的副本到工作内存中,在工作内存中操作完副本时再更新回主内存。当存在多线程时,如果工作内存A处理完还没来得及更新回主内存之前,工作内存B就从主内存中拉取了这个变量,那么很明显这个变量并不是最新的数据,会出现问题。怎么解决呢?可以使用volatile,volatile有个最显著的特性就是对它所修饰变量具有可见性,什么意思呢,就是当一个线程修改了变量的值,新的值会立刻(马上)同步到主内存中,其它线程使用时拉取到的就是最新的变量值。尽管volatile能保证变量的可见性,但并不能保证线程安全,因为它不能保证原子性。要想线程安全还是要用同步或者锁。 有一篇文档写volatile写的很好,贴一下:http://dwz.cn/76TMGW

5.线程池

JDK1.5之后引入了高级并发特性,在java.util.concurrent包中,是专门用于多线程并发编程的,充分利用了现代计算机多处理器和多核心的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的创建提供了强力的支持。

5.1线程池的5种创建方式

  • Single Thread Executor : 只有一个线程的线程池,所有提交的任务都是顺序执行; 代码: Executors.newSingleThreadExecutor()
  • Cached Thread Pool : 线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除; 代码:Executors.newCachedThreadPool()
  • Fixed Thread Pool : 拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待, 代码: Executors.newFixedThreadPool(4) 在构造函数中的参数4是线程池的大小,你可以随意设置,最好设置成和cpu的核数量保持一致,获取cpu的核数量int cpuNums = Runtime.getRuntime().availableProcessors();
  • Scheduled Thread Pool : 用来调度即将执行的任务的线程池,可能不是直接执行, 每隔多久执行一次,属于策略型的。 代码:Executors.newScheduledThreadPool()
  • Single Thread Scheduled Pool : 只有一个线程,用来调度任务在指定时间执行,代码:Executors.newSingleThreadScheduledExecutor() 示例代码如下:
    public static void main(String[] args) {
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //获取cpu核心数
        int num = Runtime.getRuntime().availableProcessors();
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(num);
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(8);
        ScheduledExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    }

5.2线程池的使用

说到线程池使用之前再强调一下Runnable的孪生兄弟——Callable,他们两个很像,只是Runnable的run方法不会有任何返回结果,主线程无法获得任务线程的返回值;但是Callable的call方法可以返回结果,但是主线程在获取时是被阻塞,需要等待任务线程返回才能拿到结果,所以Callable比Runnable更强大,那么怎么获取到这个执行结果呢?答案是Future,使用Future可以获取到Callable执行的结果。 现在开始说线程池怎么使用,也有两种方式,一种Runnable的,一种Callable的:

  • 提交 Runnable ,任务完成后 Future 对象返回 null,调用excute,提交任务, 匿名Runable重写run方法, run方法里是业务逻辑。 示例代码:
public class TestPoolWithRunnable {
    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i=0;i<10;i++){
            Future<?> submit = pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                }
            });
            System.out.println("执行结果:"+submit.get());  //所有的执行结果全是null
        }
        pool.shutdown();  //关闭线程池
    }
}
  • 提交 Callable,该方法返回一个 Future 实例表示任务的状态,调用submit提交任务, 匿名Callable,重写call方法, 有返回值, 获取返回值会阻塞,一直要等到线程任务返回结果。
/**
 * @author 刘俊重
 * Callable 跟Runnable的区别:
 * Runnable的run方法不会有任何返回结果,所以主线程无法获得任务线程的返回值
 * Callable的call方法可以返回结果,但是主线程在获取时是被阻塞,需要等待任务线程返回才能拿到结果
 */
public class TestPoolWithCallable {

    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for(int i=0;i<10;i++){
            Future<String> future = pool.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(500);
                    return "===="+Thread.currentThread().getName();
                }
            });
            //从Future中get结果,这个方法是会被阻塞的,一直要等到线程任务返回结果
            System.out.println("执行结果:"+future.get());
        }

        pool.shutdown();
    }
}

如何解决获取执行结果阻塞的问题? 在使用future.get()方法获取结果时,这个方法是阻塞的,怎么提高效率呢?如果在不要求立马拿到执行结果的情况下,可以先将执行结果放在队列里面,待程序执行完毕之后在获取每个线程的执行结果,示例代码如下:

public class TestThreadPool {

    public static void main(String[] args) throws Exception{
        Future<?> submit = null;
        //创建缓存线程池
        ExecutorService cachePool = Executors.newCachedThreadPool();

        //用来存在Callable执行结果
        List<Future<?>> futureList = new ArrayList<Future<?>>();

        for(int i = 0;i<10;i++){
            //cachePool提交线程,Callable,Runnable无返回值
            //submit = cachePool.submit(new TaskCallable(i));
            submit = cachePool.submit(new TaskRunnable(i));

            //把这些执行结果放到list中,后面再取可以避免阻塞
            futureList.add(submit);
        }
        cachePool.shutdown();
        //打印执行结果
        for(Future f : futureList){
            boolean done = f.isDone();
            System.out.println(done?"已完成":"未完成");
            System.out.println("线程返回结果:"+f.get());
        }
    }
}

把submit放在list集合中,线程直线完毕之后再取。

6.java并发编程总结

6.1不使用线程池的缺点

直接使用new Thread().start()的方式,对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有隐患:

  • 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是很大的,决不同于新建一个对象。
  • 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量。
  • 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题。

6.2线程池的类型

不管是通过Executors创建线程池,还是通过Spring来管理,都得知道有哪几种线程池:

  • FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程;
  • CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制;
  • SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行;
  • SecheduledThreadPool:周期性线程池。支持执行周期性线程任务 其实,这些不同类型的线程池都是通过构建一个ThreadPoolExecutor来完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。

6.3线程池饱和策略

由以上线程池类型可知,除了CachedThreadPool其他线程池都有饱和的可能,当饱和以后就需要相应的策略处理请求线程的任务,比如,达到上限时通过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略。

 

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

 
参考: