实战Java高并发程序设计笔记第二章

Java并行程序基础

2.1 线程必知

进程:

  • 资源分配最小单位
  • 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段

线程:

  • 程序执行的最小单位,轻量级进程
  • 线程没有独立的地址空间,它使用相同的地址空间共享数据
  • 一个进程里面可以有多个线程

为什么要使用线程?

  • 创建一个线程比进程开销小
  • CPU切换一个线程比切换进程花费小
  • 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
  • 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
  • 缺点:多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间)

线程的生命周期

初始线程:线程的基本操作

新建线程

方法一:继承Thread

Thread t = new Thread(()->System.out.println("Hello,this is created by method1"));//需重载run()方法
t.start();

不要用run()启动线程,注意调用start()和run方法的区别

方法二:实现Runnable接口

实现原理:Thread.run()方法直接调用Runnable实现类对象的run()方法,静态代理

public class CreateThread implements Runnable{
  @Override
  public void run(){
    System.out.println("Hello,this is created by method2");
  }
  public static void main(String[] args){
    Thread t = new Thread(new   CreateThread());
    t.start();
   }
}

方法三:使用Callable和future创建带返回值的线程

创建并启动有返回值的线程的步骤如下:

(1)创建。创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
(2)封装。使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
(3)启动。使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
(4)获取返回值。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class myThread3 implements Callable {

    @Override
    public String call() throws Exception {
        return "hello";
    }

    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask<>(new myThread3());
        Thread t3 = new Thread(futureTask);
        t3.start();
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


终止线程

  • 方法一:stop(),被废弃不推荐使用

为什么呢?结束线程时,会直接终止线程,会引起数据的一致性问题。可以参考TCP的四次挥手机制中,服务端收到客户端的FIN后,并不会立即关闭自己的服务,而是先发送ACK,将自己这边还未发送的数据继续发送,然后再发送FIN
使用stop()而引起的数据不一致的问题:

public class StopThreadUnsafe{
    public static User u = new User();
    public static class User{
        private int id;
        private String name;
        public User(){
            id=0;
            name="0";
        }
        //省略get,set和toString方法
    }
    public static class ChangeObjectThread extends Thread{
        @Override
        public void run(){
            whie(true){
                synchronized(u){
                    int v = (int)(System.currentTimeMills()/1000);
                    u.setId(v);
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    u.setName(v);
                    Thread.yield();
                }
            }   
        }
    }

    public static class ReadObjectThread extends Thread{
        @Override
        public void run(){
            whie(true){
                synchronized(u){
                    if(u.getId()!=Integer.parseInt(u.getName())){
                        System.out.println(u.toString());
                    }
                    Thread.yield();
                }
            }   
        }
    }
    public static void main(String[] args){
        new ReadObjectThread().start();
        while(true){
            Thread t = new ChangeObjectThread();
            t.start();
            Thread.sleep(150);
            t.stop();
        }
    }
}

如何正确停止线程呢?利用volatile修饰的标志位

public static class ChangeObjectThread extends Thread{
        volatile boolean stopme = false;
        public void stopMe(){
            stopme = true;
        }

        @Override
        public void run(){
            whie(true){
                if(stopme){
                    System.out.println("exit by stop me");
                    break;
                }

                synchronized(u){
                    int v = (int)(System.currentTimeMills()/1000);
                    u.setId(v);
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    u.setName(v);
                    Thread.yield();
                }
            }   
        }
    }


线程中断

  • 一种重要的线程协作机制,并不是使线程立即退出,而是给线程一个通知,告诉目标线程,有人需要你退出了,目标线程收到中断通知后,如何处理自行决定
  • 与中断有关的三个方法:
public void Thread.interrupt()//中断线程
public boolean Thread.isInterrupt()//判断是否被中断
public static boolean Thread.interrupted()//判断是否被中断,并清除当前中断状态
  • 与stopme()手法相似,但中断更厉害,因为wait()和sleep()这类操作也属于中断操作,能被isInterrupt()检测到
public static void main(String[] args){
        Thread t1 = new Thread();
        @Override
        public void run(){
            while(true){
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("Interrupted!");
                    break;
                }
            }

            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                System.out.println("Interrupted when sleep");
                Thread.currentThread.interrupt();
            }
            Thread.yield();
        }
        t1.start();
        Thread.sleep(2000);//让当前线程休眠若干时间,抛出一个InterruptedException中断异常
        t1.interrupt();
    }

注意:Thread.sleep()方法由于中断而抛出异常,在捕获异常后,该中断标志位会被清除,因此要再次设置中断标志位,以让下一次循环的开始检测到中断

等待(wait)和通知(notify)

  • 属于Object类的实例方法
public final void wait() throws InterruptedException
public final native void notify() 

wait()和notify()是什么:wait()必须与synchronized搭配使用,当一个对象调用wait()方法,则这个对象所处的线程就会进入object对象的等待队列,在这个等待队列中,可能有多个线程都在等待同一个object对象,当object.notify()调用后,就会从等待队列中(完全)随机选择一个线程将其唤醒
wait()和notify()的工作流程

  • 假设有两个线程T1和T2
  • T1在执行wait()方法前,先通过synchronized获取object对象的监视器,在wait()方法执行后,释放这个监视器

    为什么要释放监视器?
    目的是使得其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行

  • T2在notify()调用前,也必须获得object对象的监视器,由于T1已经释放了监视器,所以T2可以顺利获得,T2执行notify()方法唤醒等待线程
  • T1被唤醒后并不会立即去执行后续的代码,而是先尝试重新获取object的监视器,若无法获得,则必须等待这个监视器,当获得这个监视器后,T1开始执行后续代码
    案例:

public class NotifyTest {
    public final  static Object object=new Object();
    public  static class T1 extends  Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T1 start");
                System.out.println(System.currentTimeMillis()+": T1 wait for object");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis()+": T1 end");
            }
        }
    }

    public static class T2 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T2 start notify object");
                object.notify();
                System.out.println(System.currentTimeMillis()+":T2 end");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1=new T1();
        Thread t2=new T2();
        t2.start();
        t1.start();
    }
}

Object,.wait()和Thread.sleep()都能让线程等待若干时间,wait()方法需要被唤醒,在唤醒后hi释放目标对象的锁,而sleep()不会释放任何资源

挂起(suspend)和继续执行(resume)

  • 废弃不推荐使用
  • 如果rusume在suspend前执行,那么被挂起的线程所占用的锁不会被释放,并且其状态为Runnable,影响对当前状态的判断
    例子说明:

如何实现一个可靠的suspend操作呢?利用wait()和notify()

notifyAll():唤醒等待队列中的所有线程,让他们竞争锁

等待线程结束(join)和谦让(yield)

join

  • 应用场景:当一个线程的输入可能依赖于另外一个或多个线程的输出时,当前线程就需要等待依赖线程执行完毕,才能执行
  • jdk提供的两个join()操作

    //无限期等待,当前线程在目标线程执行完毕前一直阻塞,当前线程一直等着目标线程完毕

    public final void join() throws InterruptedException
    

    //当前线程会在规定时间内等待目标线程,一旦超过规定时间,当 前线程就不等了,继续向下执行

    public final synchronized void join(long millis) throws InterruptedException
    
  • 实例程序:
  • join的本质:让调用线程wait()在当前线程对象实例上
      while(isAlive()){
          wait(0); 
       }
    

yield

  public static void yield();

使当前线程让出CPU后继续争夺资源

volatile与Java内存模型(JMM)

volatile

  • volatile无法保证一些符合操作的原子性,例如i++;
  • 可以保证数据的可见性和有序性

分门别类的管理:线程组

驻守后台:守护线程(Daemon)

  • 什么是守护线程?当一个Java应用中只有守护线程时,JVM就会退出
  • 注意:setDaemon需在start之前执行

先干重要的事:线程优先级

  • 线程优先级高的并不一定先执行,只是拥有更多可以执行的机会

线程安全的概念与synchronized

  • 什么是线程安全?

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的

  • 线程不安全的例子:
  • synchronized介绍

作用:实现线程间的同步
工作:对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性

  • synchronized的多种用法

指定加锁对象:
直接作用于实例方法:
直接作用域静态方法:
synchronized实现线程安全的i++,保证复合操作的原子性

注意:使用Runnable让两个线程关注一个同一个对象锁
错误示范:

使用synchronized的第三种方法进行修正

  • synchronized除了线程同步、确保线程安全,还可以保证线程间的可见性和有序性(多个线程串行执行)

程序中的幽灵:隐蔽的错误

出现异常的错误至少可以发现,没有异常的错误例如数据溢出就很难排查

无提示的错误案例

并发下的ArrayList

例子:

可能出现的三种情况

  • 程序正常结束
  • 程序抛出数组越界异常
  • 没有提示的错误
    改进方法:使用线程安全的Vector代替ArrayList

并发下诡异的HashMap

HashMap的源码解析
Jdk 8以前为什么 HashMap在多线程下的put操作容易导致链表成环

初学者常见问题:错误的加锁

public class BadLockOnInteger implements Runnable{
    public static Integer i=0;
    static BadLockOnInteger instance = new BadLockOnInteger();
    @Override
    public void run(){
        for(int j=0;j<10000000;j++){
            synchronized(i){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t1 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

为什么最终的结果不是20000000?

在Java中Integer属于不变对象,每次对Integer对象进行加法操作时,实际上是新创建了一个Integer对象来表示加完后的结果,因此i++的本质是创建了一个新的Integer对象,并将其引用赋给i,所以在多个线程间,并不一定能够看到同一个对象,代码中两个线程每次加锁可能都在了不同的对象实例上
如何修正?

synchronized(instance)

你可能感兴趣的:(实战Java高并发程序设计笔记第二章)