并发与多线程开发

认识线程

  线程的启动方式以及应用场景
  线程的状态和常用方法
  线程的优先级 提升任务的响应速度
  线程间通讯 (子线程与主线程 发消息)

多线程开发

  线程安全 (关键字synchornized 锁 , 原子类, 并发容器)
  线程流程控制

线程池原理

  任务调度
  复用原理

多线程优化

 线程池
 并发安全
 kotlin(协程)

线程与进程
分类:ui线程 工作线程

5种线程创建方式

  • new Thread (2种创建方法)
    缺点: 却反统一管理,可能无限制新建线程,相互之间竞争,可能占用过多系统资源导致OOM 或者死机.
public class ThreadDemo {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("线程2 =" + i);
                }
            }
        });

        t1.start();
        t2.start();
        
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 10; i++) {
                System.out.println("线程1 = " + i);
            }
        }
    }
}
  • AsyncTask (3种写法)
    于是Android 提供了 AsyncTask, 轻量级任务工具类,提供任务执行的进度回调到UI线程,
    场景:如果需要知道任务执行的进度,多个任务串行
    缺点:生命周期和宿主的生命周期不同不,有可能会导致内存泄漏,默认情况下所有任务都是串行执行。
    解决方法 可以加上static不持有宿主的对象,如果程序不能并发执行,就指定一个线程池.
public class AsyncTaskDemo  extends AsyncTask {

    @Override
    protected String doInBackground(String... params) {
       //TODO 。。。。。
        publishProgress(**);
        return params[0];
    }

    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(s);
        // 拿到结果  result
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        // 拿到进度 , values[0];
    }
    
}

main(){
        AsyncTaskDemo asyncTask = new AsyncTaskDemo();

        // #1, 默认串行执行,可以感知任务进度
        asyncTask.execute( params );
        //  可以修改为并发执行
        asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);


        // #2,如果不想知道任务执行进度,可以直接创建
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                // ......
            }
        });

        //# 3,直接创建 并发执行
        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {

            }
        });
}
  • HandlerThread
    使用于主线程需要和工作线程通信,适用于持续性任务,比如轮询,所有任务串行执行。
    缺点: 不会像普通线程一样主动销毁资源,会一直运行,所以可能会造成内存泄漏。
 private void handlerThread() {
        // 创建一个子线程
        HandlerThread thread = new HandlerThread("current-thread");
        thread.start();
        
        /**
         *  在主线程中新建一个Handler,持有 Thread 的 looper
     Looper.
         *  现在是主线程中的Handler 持有了一个 子线程的 Looper
         */
        MHandler handler = new MHandler(thread.getLooper());

        // handler 发送数据 ,就是主线程 向子线程发送消息
        handler.sendEmptyMessage(1);


    }


    static class MHandler extends Handler {
        public MHandler(@NonNull Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            //  因为 Handler 持有了子线程的 Looper,所以此时实在子线程当中。
            Log.e("MHandler", "message = " + msg.what);
           
        }
    }


如果 
  MHandler handler = new MHandler();  // 没有传递Looper
  handler.sendEmptyMessage(1);
  // 因为主线程中创建 Handler 时会有个默认的  Looper
  //这时捕获消息的就是再主线程 中.
  • IntentService
    继承与Service ,在创建IntentService时,onCreate()内部会创建一个子线程来完成耗时操作
    应用场景: 适用于任务需要夸页面读取任务执行的进度、结果。比如后台上传图片,批量操作数据库等。任务执行完成后,就会自我结束,不需要手动 stopService()(与Service的区别)
public class MyIntentService extends IntentService {

    public MyIntentService() {
        super("MyIntentService");
    }


    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent != null) {
            String value = intent.getStringExtra("key");
            Log.e("MyIntentService", "传递 key = " + value);
        }
    }

}

main 中调用

       Intent intent = new Intent(this, MyIntentService.class);
        intent.putExtra("key", "value");
        startService(intent);

  • ThreadPoolExecutor
    使用与快速处理大量耗时较短的任务场景。
Executors.newCachedThreadPool();            // 线程可复用的线程池
Executors.newFixedThreadPool();             // 固定线程数量的线程池
Executors.newScheduledThreadPool();         // 可指定定时任务的线程池
Executors.newSingleThreadExecutor();        // 线程数量为1 的线程池.

线程的优先级

一般而言,线程的优先级越高,获得Cpu片的概率越大。

       /**
         *  Jdk 提供的 , [1,10] 数字越大,优先级越高, UI线程的 优先级 为 5 ;
         *
         */
        Thread thread = new Thread();
        thread.setPriority(1);


        /**
         * Android API , 优先级更精细的划分为 [-20,19],数字越小,优先级越高, UI线程的 优先级为 -10.
         * 效果较为明显
         * 一般把耗时较长的线程的优先级设置较低级别,
         * 耗时较短较频繁的线程的优先级设置较高级别. 但是不要高于UI 线程.
         */
         android.os.Process.setThreadPriority();

线程的状态及常用方法

image.png

wait()/notify()
  当前线程进入等待状态,并且释放资源对象锁,可使用notify(),notifyAll() 或者等待超时来唤醒.
使用与多线程同步,一个线程要等待另一个线程的结果或者部分结果。 注意 wait - notify 的执行顺序

 final Object object = new Object();
        class Runnable1 implements Runnable {
            @Override
            public void run() {
                Log.e("Runnable", "thread1 - start");

                synchronized (object) {
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Log.e("Runnable", "thread1 - end");
            }
        }

        class Runnable2 implements Runnable {

            @Override
            public void run() {
                Log.e("Runnable", "thread2 - start");
                
                synchronized (object) {
                    object.notify();
                }  
                
                /**
                 *  线程2 notify()唤醒了线程1 ,
                 *  如果线程2有耗时操作,就先执行线程1 了.
                 */
          //      try {
          //          Thread.sleep(1000);
           //     } catch (InterruptedException e) {
           //         e.printStackTrace();
           //     }
               
                Log.e("Runnable", "thread2 - end");
            }
        }
        new Thread(new Runnable1()).start();
        new Thread(new Runnable2()).start();

以上, object就是对象锁, 这里如果换成 this , 跟object有啥不同吗。 wait()的使用是要在 synchornized() 同步代码块里面执行的.

理想状态下,线程1 执行了 wait()以后使当前线程进入等待状态,于是线程2拿到Cpu片开始执行,然后通过notify()唤醒线程1。
但是实际情况,线程1 和线程2 谁先执行可不知道,如果线程2先执行了,轮到线程1执行时,到了wait() 就没有唤醒了出现假死现象。

为了规避这种情况,可以定义一个 volatile boolean hasNotify = false, 线程2 notify() 以后 hsNotify = true ; 在 线程1 wait()前判断 if(!hasNotify){ wait() } , 或者 wait(1000), 自动唤醒,可以避免线程 一直 wait();

join()
    在当前线程中插入一条任务,当任务完成以后线程才可以继续执行.

 class JoinThread extends Thread {
            @Override
            public void run() {
                super.run();
                Log.e("JoinThread", "run: 1 " + System.currentTimeMillis());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("JoinThread", "run:2 " + System.currentTimeMillis());
            }
        }

        JoinThread thread = new JoinThread();
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.e("JoinThread", "join: 3 " + System.currentTimeMillis());
}
执行顺讯 就是  1,2,3
UI线程中插入了一个 JoinThread,UI线程要等JoinThread执行完毕以后才接着执行。

yield()
   不咋的用
暂停当前正在执行的线程对象,不会释放资源锁。
sleep()
   使调用线程进入休眠状态,但在一个synchornized块中执行sleep,线程虽然休眠,但不会释放资源对象锁.

 final Object object = new Object();
        class Runnable1 implements Runnable {
            @Override
            public void run() {
                Log.e("Runnable", "thread1 - start");

                synchronized (object) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Log.e("Runnable", "thread1 - end");
            }
        }

        class Runnable2 implements Runnable {
            @Override
            public void run() {
                synchronized (object) {
                    Log.e("Runnable", "thread2 - start");
                    Log.e("Runnable", "thread2 - end");
                }
            }
        }
        new Thread(new Runnable1()).start();
        new Thread(new Runnable2()).start();

线程1 和线程2 的执行顺序不保证,如果线程1先执行,线程2会等线程1全部执行完才执行.

线程间通信

子线程发主线程发送消息
      post() ,runOnUiThread()...

主线程向子线程发送消息

 /**
         * 要让主线程给子线程发送消息, 必然要使子线程有处理消息的能力 ,Looper
         */
        class LooperThread extends Thread {
            Looper looper;

            public LooperThread(String name) {
                super(name);
            }

            // 但是由于 Thread 可能不会一 start 就执行 run(),
            // 所以 looper可能为空
            // 于是  wait();
            public Looper getLooper() {
                if (looper == null && isAlive()) {
                    synchronized (this) {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return looper;
            }

            @Override
            public void run() {
                Looper.prepare();

                looper = Looper.myLooper();
                synchronized (this) {
                    notify();
                }
                // 开启Looper 的无线循环;
                Looper.loop();
            }
        }


        LooperThread looperThread = new LooperThread("Looper - Thread");
        looperThread.start();

        // 创建的 Handler 持有了子线程的Looper, handler 就会捕获有子线程中Looper分发的消息.
        Handler handler = new Handler(looperThread.getLooper()) {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);

                Log.e("ThreadMessage", " message =  " + msg.what);
                Log.e("ThreadMessage", "thread - name: " + Thread.currentThread().getName());
            }
        };

        handler.sendEmptyMessage(2);

线程安全

(买票)
线程安全的本质:

    能够让并发线程,有序的运行(有序可能是先来后到的排队,也可能是有人插队,但同一时刻只能有一个线程有权访问同步数据), 线程执行的结果,能够对其他线程可见。

AutomicInteger 原子类

    实现无锁数据更新,自旋( do-while 循环 ,线程不会阻塞) 的设计能够有效避免线程阻塞-唤醒的系统资源开销。 客户端并发量不会很高.
适用于多线程技术,原子操作,并发量小的场景。

volatile 可见性修饰

    volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫将变化的值重新写入共享内存
不能解决非原子操作的线程安全性,性能不及原子类高。

 volatile int count;
    private void increment() {

        // 其他线程可见.
        count = 5;

        // 以下两步操作都不是一步操作,是非原子操作,所以不可被其他线程可见.
        count = count + 1;
        count++;
    }

对比两种方式

public class AtomicDemo {

    public static void main(String[] args) {

        final AtomicTask task = new AtomicTask();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 10000; i++) {
                    task.atoAdd();
                    task.volatileCount();
                }

            }
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("原子类操作的结果 = " + task.atomicInteger.get()); // 结果  20000
        System.out.println("volatile 操作的结果 = " + task.count);   // 结果 18720       
    }
    
    static class AtomicTask {
        AtomicInteger atomicInteger = new AtomicInteger();
        volatile int count = 0;
        public void atoAdd() {
            // 增量 ,先 加1,然后返回操作之前的值.
            atomicInteger.getAndIncrement();

            // 减量 ,先获取未更改前的值,然后 减1
            //atomicInteger.getAndDecrement();
        }
        public void volatileCount() {
            count++;
        }
    }
}

以上,在输出结果中表现 使用volatile 修饰的变量 不保证线程安全。

synchornized

锁java对象, 锁Class类对象,锁代码块.

  • 锁方法 , 加在方法上,为获取到对象锁的其他线程不可以访问该方法
public class ThreadDemo {

    static ArrayList tickets = new ArrayList<>();
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            tickets.add("票 -" + (i + 1));
        }
        saleTicket();
    }
    
    private static void saleTicket() {
        final SynchronizedDemo demo = new SynchronizedDemo();
        for (int i = 0; i < tickets.size(); i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.buyTicket();
                }
            }).start();
        }
    }

    static class SynchronizedDemo {
        synchronized void buyTicket() {
            String name = Thread.currentThread().getName();
            System.out.println("买票人:" + name + " 已经准备好");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("买票人 :" + name + " 买到的票是 " + tickets.remove(0));
        }

    }
}
==========  输出结果 ============
买票人:Thread-0 已经准备好
买票人 :Thread-0 买到的票是 票 -1
买票人:Thread-4 已经准备好
买票人 :Thread-4 买到的票是 票 -2
买票人:Thread-3 已经准备好
买票人 :Thread-3 买到的票是 票 -3
买票人:Thread-2 已经准备好
买票人 :Thread-2 买到的票是 票 -4
买票人:Thread-1 已经准备好
买票人 :Thread-1 买到的票是 票 -5

以上, 针对同一个 买票人对象,在butTicket()方法上加 synchronized就可以保证线程安全. 如果在多线程中访问不同对象的同步方法,就不能保证线程安全了.
因为线程同步是对同一个对象而言.
比如这样, 每个线程中都创建一个买票人对象,

for (int i = 0; i < tickets.size(); i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    final SynchronizedDemo demo = new SynchronizedDemo();
                    demo.buyTicket();
                }
            }).start();
        }

==========  输出结果 ============
买票人 :Thread-3 买到的票是 票 -2
买票人 :Thread-0 买到的票是 票 -1
买票人 :Thread-2 买到的票是 票 -4
买票人 :Thread-1 买到的票是 票 -3
买票人 :Thread-4 买到的票是 票 -1

=========  == 购买的票出现了重复 =====
  • 锁Class 对象, 加载 static 方法上相当于给Class对象加锁,哪怕是不同的Java对象实例,也需要排队等候.
    如果 synchronized 加在 static方法上,线程安全
 static class SynchronizedDemo {
        synchronized static void buyTicket() {
            String name = Thread.currentThread().getName();
            System.out.println("买票人:" + name + " 已经准备好");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("买票人 :" + name + " 买到的票是 " + tickets.remove(0));
        }
    }
=========== 输出结果 ==========
买票人:Thread-0 已经准备好
买票人 :Thread-0 买到的票是 票 -1
买票人:Thread-4 已经准备好
买票人 :Thread-4 买到的票是 票 -2
买票人:Thread-3 已经准备好
买票人 :Thread-3 买到的票是 票 -3
买票人:Thread-2 已经准备好
买票人 :Thread-2 买到的票是 票 -4
买票人:Thread-1 已经准备好
买票人 :Thread-1 买到的票是 票 -5

以上,static加锁,相当于给Class类加锁, 在内存中只有一个,即使不同的买票人对象来访问,也是可以保证线程安全的.

  • 锁代码块,获取到对象锁的线程可以执行同步代码块以外的代码.
 void buyTicket() {
            String name = Thread.currentThread().getName();

            synchronized (this) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("买票人 :" + name + "正在买票");
            }
            System.out.println("买票人 :" + name + " 买到的票是 " + tickets.remove(0));
        }
================ 输出结果 ===================
买票人 :Thread-1正在买票
买票人 :Thread-1 买到的票是 票 -1
买票人 :Thread-3正在买票
买票人 :Thread-3 买到的票是 票 -2
买票人 :Thread-4正在买票
买票人 :Thread-4 买到的票是 票 -3
买票人 :Thread-2正在买票
买票人 :Thread-2 买到的票是 票 -4
买票人 :Thread-0正在买票
买票人 :Thread-0 买到的票是 票 -5

以上 如果 synchronized (SynchronizedDemo.class){ .........买票...... }
能保证线程同步.

synchronized 优劣势
优势:可以规避死锁,即使同步代码快中出现了异常,jvm也能自动释放锁。
劣势: - 必须要获取锁对象的线程执行完或者异常才可以释放锁,不能中断
      - 多个线程获取锁 是否成功也不知道,不够灵活.
      - 不能设置超时。

ReentrantLock锁

void lock() ;                 获取不到会阻塞
boolean tryLock() ;           尝试获取锁,获取到锁返回 true,

基本用法 :

public class ReentrantLockDemo {

    public static void main(String[] args) {
        final ReentrantTask task = new ReentrantTask();

        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {

                    task.buyTicket();
                }
            }).start();
        }
    }


    static class ReentrantTask {
        ReentrantLock lock = new ReentrantLock();

        void buyTicket() {
            String name = Thread.currentThread().getName();
            
            try {
                lock.lock();
                System.out.println(name + " :准备买票");
                Thread.sleep(100);
                System.out.println(name + " : 票买好了");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
=============== 输出结果 ============
Thread-0 :准备买票
Thread-0 : 票买好了
Thread-1 :准备买票
Thread-1 : 票买好了
Thread-2 :准备买票
Thread-2 : 票买好了
Thread-3 :准备买票
Thread-3 : 票买好了
Thread-4 :准备买票
Thread-4 : 票买好了
Thread-5 :准备买票
Thread-5 : 票买好了

可重入,避免死锁
在不释放锁的情况下,重复获取锁的对象.

            try {
                lock.lock();
                System.out.println(name + " :准备买票");
                Thread.sleep(100);
                System.out.println(name + " : 票买好了");


                lock.lock();
                System.out.println(name + " :准备买票");
                Thread.sleep(100);
                System.out.println(name + " : 票买好了");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                lock.unlock();
            }

==============  输出结果 ==============
Thread-0 :准备买票
Thread-0 : 票买好了
Thread-0 : 又准备买票
Thread-0 : 票又买好了
Thread-1 :准备买票
Thread-1 : 票买好了
Thread-1 : 又准备买票
Thread-1 : 票又买好了
Thread-2 :准备买票
Thread-2 : 票买好了
Thread-2 : 又准备买票
Thread-2 : 票又买好了
Thread-3 :准备买票
Thread-3 : 票买好了
Thread-3 : 又准备买票
Thread-3 : 票又买好了
Thread-4 :准备买票
Thread-4 : 票买好了
Thread-4 : 又准备买票
Thread-4 : 票又买好了

公平锁与非公平锁
公平锁 : 所有进入阻塞队列的线程依次有机会执行.
(默认)非公平锁:允许线程插队,避免每一个线程都阻塞再被唤醒,性能较高。但是因为可以插队的原因,会导致有的线程一直得不到执行.

ReentrantLock lock = new ReentrantLock( true / false );

简单模拟多线程打印功能

public class ReentrantDemo2 {

    public static void main(String[] args) {

        final PrintTask task = new PrintTask();

        for (int i = 0; i < 5; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    task.print();
                }
            }).start();
        }
        
    }


    static class PrintTask {
        private ReentrantLock lock = new ReentrantLock(true);

        void print() {
            String name = Thread.currentThread().getName();
            try {
                lock.lock();
                System.out.println(name + ": 第一次打印");
                Thread.sleep(1000);
                lock.unlock();


                lock.lock();
                System.out.println(name + ": 第二次打印");
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    
}

如果 公平锁
输出结果

Thread-0: 第一次打印
Thread-1: 第一次打印
Thread-2: 第一次打印
Thread-3: 第一次打印
Thread-4: 第一次打印
Thread-0: 第二次打印
Thread-1: 第二次打印
Thread-2: 第二次打印
Thread-3: 第二次打印
Thread-4: 第二次打印

如果非公平锁

Thread-0: 第一次打印
Thread-0: 第二次打印
Thread-1: 第一次打印
Thread-1: 第二次打印
Thread-2: 第一次打印
Thread-2: 第二次打印
Thread-3: 第一次打印
Thread-3: 第二次打印
Thread-4: 第一次打印
Thread-4: 第二次打印

以上,表明公平锁释放锁以后,每个线程获取到锁的机会是均等的, 如果是非公平锁,当第一次打印完释放锁以后,因为其他线程的状态都是阻塞的,重新唤醒需要有一定的资源消耗,而当前线程释放掉以后还没有进入阻塞状态,由当前线程来获取锁对象是开销最小的。

应用场景:
公平锁:交易
非公平锁 :synchornized, 场景就很多了.

ReentrantLock- Condintion对象

使用 await - singnal 可以指定唤醒一个(或一组)线程。

共享锁 & 排他锁 (ReentrantReadWriteLock)

共享锁,所有线程都可同时获得,并发量高,比如在线查看文档
排他锁,同一时刻只能有一个线程有权修改资源,比如在线文档编辑.

  如何正确的使用锁和原子类

一般情况下 synchronized 已经足够使用,
如果需要知道锁的细节,就要使用Lock

   调优

  • 减少持锁的时间,比如synchronized 同步代码快,非持有锁的线程可以执行同步代码块之外的程序,可以减少线程等待时间.
  • 锁粗化 , 如果多次同步代码块之间有一部分耗时很短不用同步的代码,可以将这些程序合并为一次同步.
  • 在并发量不大的情况下,可以考虑使用原子类。

线程池

  为什么引入线程池

  • 降低资源消耗 : 通过重复利用已创建的线程降低线程创建和销毁的造成的消耗。
  • 提高响应速度: 当任务到达时,任务可以不用等待线程的创建就可以立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会造成系统资源的浪费,还会影响系统的稳定性,使用线程池统一分配,调优和监控.

  Java中默认的线程池

  线程池实现原理

  线程池中线程复用的原理

你可能感兴趣的:(并发与多线程开发)