多线程【学习】自【java编程的逻辑】

多线程

1.关于线程的认识

1.1创建线程的方式以及线程的理解

线程

一个程序的运行就叫做进程,进程是系统资源的分配,是不可控的;同时一个进程可以拥有多个线程;控制线程是我们能够做的。

线程表示一条单独的流,它有自己的程序计数器,有自己的栈

创建线程方式

(1)继承Thread,也就是构造Thread类

实现线程的执行单元(3种方式)

(1)继承Thread,重写run方法

(2)实现Runnable接口,实现run方法

(3)实现Callable接口,实现run方法

线程的资源共享

简单来说:就是多个线程多同一个资源的访问

  • static修饰符修饰

银行叫号

package threadDemo;

/**
 * 银行叫号,模拟资源共享,线程安全问题
 * 不考录资源共享问题
 */
public class threadDemo4 extends Thread{
    private final String name;
    private int index = 1;//此时不做static修饰
    private final static int max = 50;

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

    @Override
    public void run() {
        while (index <= max) {
            System.out.println("柜台:"+name+"当前的号码是"+(index++));
            try {
                Thread.sleep(100);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        threadDemo4 t = new threadDemo4("1号柜台");
        t.start();
        threadDemo4 t2 = new threadDemo4("2号柜台");
        t2.start();
        threadDemo4 t3 = new threadDemo4("3号柜台");
        t3.start();
        threadDemo4 t4 = new threadDemo4("4号柜台");
        t4.start();
    }
}

考虑共享资源唯一性问题

package threadDemo;
/**
 * 银行叫号,模拟资源共享,线程安全问题
 * 对共享资源用static修饰
 */
public class threadDemo5 extends Thread{
    private final String name;
    private static int index = 1;//此时不做static修饰
    private final static int max = 50;

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

    @Override
    public void run() {
        while (index <= max) {
            System.out.println("1柜台:"+name+"当前的号码是"+(index++));
            try {
                Thread.sleep(100);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        threadDemo5 t = new threadDemo5("1号柜台");
        t.start();
        threadDemo5 t2 = new threadDemo5("2号柜台");
        t2.start();
        threadDemo5 t3 = new threadDemo5("3号柜台");
        t3.start();
        threadDemo5 t4 = new threadDemo5("4号柜台");
        t4.start();
    }
}

  • 实现Runnable接口,交由它来解决
package threadDemo;


public class threadDemo6 implements Runnable{
    private int index = 1;
    private final static  int max = 50;

    @Override
    public void run() {
        while(index <= max) {
            System.out.println(Thread.currentThread()+":的号码是:"+(index++));
            try {
                Thread.sleep(100);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        final threadDemo6 t = new threadDemo6();
        Thread thread = new Thread(t, "1号窗口");
        Thread thread1 = new Thread(t, "2号窗口");
        Thread thread2 = new Thread(t, "3号窗口");
        Thread thread3 = new Thread(t, "4号窗口");

        thread.start();
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

**注意:**无论是static,还是Runnable接口,他们都存在缺陷,当资源数过大时,都会存在某个资源被重复使用,或者某个资源没有使用到的情况。这就是:线程安全问题

1.2线程的生命周期

  • new:创建
  • runnable:就绪,等待cpu资源的调度
  • running:运行,获取到cpu资源
  • blocking:进入阻塞状态
    • sleep,wait,等待获取某个资源的锁,阻塞的io操作
  • terminated:线程结束,不可以再切换到任何状态

1.3线程的基本属性和方法

**线程有一些基本属性和方法,包括id,name,优先级(priority),状态,是否daemon线程,sleep方法,wait方法,yield方法,join方法,过时的方法等 **

1.4总结

常用的线程方法

  • run
  • start
  • wait
  • sleep
  • yield():会让出cpu资源,sleep和wait不会
  • join
1.让调用join的线程等待该线程的结束,但是在等待的过程中,这个等待可能会被中断,抛出:中断异常

2.join方法的变体:可以限定等待的最长时间,单位为毫秒,如果为 0 ,则表示无限期
  • interrupt
  • notify/notifyAll

出现的设计模式

  • 模板模式:Thread类写好run方法的逻辑,具体的业务实现填充交由子类自己
  • 策略模式:Thread类的run方法,将线程的控制本身,和业务逻辑分离开来。做到单一职责原则。

Thread类的start方法和run方法

1.当start方法被调用时,这时会开辟start方法的线程,用来执行它下面的方法,比如:run方法;同时main方法也在执行

1.5线程的优点和缺点

优点

  • 可用充分利用多cpu的计算能力,单线程只能利用一个cpu,使用多线程可以利用多cpu的计算能力。
  • 充分利用硬件资源,cpu和硬盘,网络是可以同时工作的,一个线程在等待网络io的同时,另一个线程完全可以利用cpu,对于多个独立的网络请求,完全可以使用多个线程同时请求。
  • 在用户界面应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,否则,如果所有的事情都是一个线程在执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
  • 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种请求,以及各种网络和文件IO事件,建模和编写程序要容易的多。

缺点

  • 操作系统会为每个线程创建必要的数据结构,栈,程序计数器等,创建也需要一定的时间。
  • 线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前cpu寄存器的值,程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来上下文的状态,整个过程称之为上下文切换,这个切换不仅耗时,而且使cpu中的很多缓存失败。

1.6关于线程状态

2.关于并发

首先是关于执行流(线程),内存,和程序代码之间的关系。

1)当多条执行流执行相同的程序时,每条流都有自己的栈,方法中的参数和局部变量都有自己的一份。但是当多条流可以操作相同的变量时,可能会出现一些意料之外的情况,包括竟态条件和内存可见性问题。

竞态条件

竞态条件(race condition):当多个线程访问和操作同一个对象时,最终执行结果与执行顺寻有关,可能正确可能不正确。

  • 示例:
package threadDemo;

//竟态条件示例
public class threadDemo10 extends Thread{
    private static int counter = 0;

    //synchronized
    @Override
    public  void run() {
        for (int i = 0; i < 1000; i++) {
            counter++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 10000;
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            threads[i] = new threadDemo10();
            threads[i].start();
        }
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter);
    }
}
//结果:期待值1000万,结果只有900多万

解决方法:

  • 使用synchronized关键字
  • 使用显式锁
  • 使用原子变量

2.内存可见性

多个线程可用共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到。

示例

	package threadDemo;

public class threadDemo7 {
    private static boolean shutDown = false;
    static class HelloThread extends Thread {
        @Override
        public void run() {
            while (!shutDown) {
                //do-nothing
                System.out.println("............");
            }
            System.out.println("exit hello");
        }

    }
    public static void main(String[] args) throws InterruptedException {
        new HelloThread().start();
        Thread.sleep(1000);
        shutDown = true;
        System.out.println("exit main");
    }
}

内存可见性问题:

在计算机系统中,除了内存,数据,还有被缓存到cpu寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或者cpu缓存中获取,而不一定到内存中取,当修改一个变量时,也可能先写到缓存中,稍后才会更新到内存中。在单线程的程序中,这一般不是问题,但是在多线程的程序中,尤其时在有多cpu的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有即时同步到内存,二是另一个线程根本就没有从内存读。

解决方法:

  • 使用volatile关键字
  • 使用synchronized关键字,或者显式锁同步

2.1synchronized

synchronized可以用于修饰类的实例方法,静态方法和代码块。

1)synchronized修饰实例方法

示例:

synchronized修饰计数方法

package threadDemo;

public class Count {
    private int count;
    public synchronized void incr() {
        count++;
    }
     public synchronized int getCount() {
        return count;
    }
}

package threadDemo;


public class threadDemo8 extends Thread{
    Count counter;

    public threadDemo8(Count counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.incr();
        }
    }
    

    public static void main(String[] args) throws InterruptedException {
        int num = 10000000;
        Count counter = new Count();
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            threads[i] = new threadDemo8(counter);
            threads[i].start();
        }
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter.getCount());
    }
}

多线程可以同时执行同一个synchronized实例方法,只要他们访问的对象是不同的即可。所以synchronized实例方法保护的是当前实例对象,即this,this对象有一个锁一个等待队列,锁只能被一个线程持有,其他尝试获得同样锁的线程需要等待。

执行synchronized实例方法的过程如下:

  1. 尝试获得锁,如果当前能够获得锁,继续下一步,否则加入等待队列,阻塞并等待唤醒。
  2. 执行实例方法体代码。
  3. 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性。

强调:

synchronized保护的是对象而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。

所以一般在保护变量时,需要在所有访问变量的方法上加上synchronized

2)synchronized修饰静态方法

 private int count;
    public synchronized synchronized void incr() {
        count++;
    }
     public synchronized synchronized int getCount() {
        return count;
    }

synchronized修饰实例方法,保护的是当前实例对象this;对于静态方法,保护的是类对象。

实际上,每个对象都有一个锁和一个等待队列,类对象也不例外。

synchronized静态方法和synchronized实例方法保护的是不同的对象,不同的两个线程,可以一个执行synchronized静态方法,另一个执行synchronized实例方法。

3)synchronized修饰代码块

public class Count {
    private int count;
    public  void incr() {
        synchronized(this) {
          count++;
        }
    }
     public  int getCount() {
       synchronized(this) {
          return count;
        } 
    }
}

synchronized括号里修饰的就是要保护的对象,对于实例方法来说,就是this。

public class Count {
    private int count;
    public static void incr() {
        synchronized(Count.class) {
          count++;
        }
    }
     public static int getCount() {
       synchronized(Count.class) {
          return count;
        } 
    }
}

这是对于静态方法。

synchronized同步的对象可以是任意的对象,任意对象都有一个锁和等待队列,或者说,任何对象都可以作为锁对象。

public class Count {
    private int count;
  	private Object lock = new Object();
    public static void incr() {
        synchronized(lock) {
          count++;
        }
    }
     public static int getCount() {
       synchronized(lock) {
          return count;
        } 
    }
}
2.1.2进一步理解synchronized
  • 可重入性
  • 内存可见性
  • 死锁

1.可重入性

可重入性:就是如果一个执行线程,在它获得了锁之后,在调用其他需要获取锁的代码时,可以直接调用。但是不是所有的锁都是可重入的。
可重入是通过记录锁的持有线程和持有数量来实现的。
当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放时,减少持有量,当数量变成0时才释放整个锁。

2.内存可见性

对于一些复杂操作,synchronized可以实现原子操作,避免出现竞态事件。

synchronized除了保证原子操作外,还有一个重要作用就是:保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读取最新数据。

但是使用synchronized来保证内存可见性成本有点高;轻量级的方式,使用volatile给变量做修饰。

private volatile int counter;

加入了volatile之后,java会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存值。

3)死锁

死锁:有a,b两个线程,a持有锁A,在等待锁B;b持有锁B,在等待锁A;a和b陷入了互相等待。

package threadDemo;


//死锁
public class threadDemo9 {
    private static Object lockA = new Object();
    private static Object lockB = new Object();
    private static void startThreadA() {
        Thread threadA = new Thread() {
            @Override
            public void run() {
                synchronized (lockA) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lockB) {
                    }
                }
            }
        };
        threadA.start();
    }

    private static void startThreadB() {
        Thread threadB = new Thread() {
            @Override
            public void run() {
                synchronized (lockB) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lockA) {
                    }
                }
            }
        };
        threadB.start();
    }
    public static void main(String[] args) {
        startThreadA();
        startThreadB();
    }
}

解决:

  • 首先,应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁

  • 显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或者干脆放弃,以避免死锁。

2.2同步容器及注意事项

同步容器:Collection中有synchronized修饰的容器。

但是也不是绝对安全,需要注意以下情况:

  • 复合操作,比如先检查再更新
  • 伪同步
  • 迭代

1.复合操作

2.伪同步

同步错对象,所有的方法必须使用相同的锁。

3.迭代

对于同步容器对象,虽然单个操作是安全的,但是迭代并不是。

示例

创建一个同步List对象,一个线程修改list,另一个遍历

package threadDemo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class threadDemo11 {
private static void startMotifyThread(final List<String> list) {
    Thread motifyThread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                list.add("item " + i);
                try {
                    Thread.sleep((int) (Math.random() * 10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    motifyThread.start();
}
    private static void startIteratorThread(final List<String> list) {
        Thread iteratorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                        for (String s : list) {
                    }
                }
            }
        });
        iteratorThread.start();
    }

    public static void main(String[] args) {
        final List<String> list = Collections.synchronizedList(new ArrayList<String>());
        startIteratorThread(list);
        startMotifyThread(list);
    }
}

抛出异常

Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at threadDemo.threadDemo11$2.run(threadDemo11.java:31)
	at java.lang.Thread.run(Thread.java:748)

意思是:在遍历的同时容器发送了结构性变化,就会抛出该异常。同步容器并没有解决这个问题;

解决:

需要在遍历的时候给整个容器对象加锁

 private static void startIteratorThread(final List<String> list) {
        Thread iteratorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                        synchronized (list) {
                        for (String s : list) {
                        }
                    }
                }
            }
        });
        iteratorThread.start();
    }

4)并发容器

  • CopyOnWriteArrayList
  • ConcurrentHashMap
  • ConcurrentLinkedQueue
  • ConcurrentSkipListSet

2.3线程的基本协作机制

线程的基本协作机制:wait/notify(Object类的方法)

wait

wait能做什么?

  • 首先每个对象都有一把锁和等待队列,以及条件队列(用与线程之间的协作);
  • 调用wait会把当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,它需要等待一个条件,这个条件它自己改变不了,需要其他线程去改变。当其他线程改变条件以后,应该调用Object的notify方法。

notify

  1. notify
从条件队列中选出一个线程,将其从队列中移除并唤醒
  1. notifyAll
移除条件队列中所有的线程并全部唤醒

注意:wait/notify方法只能在synchronized代码块中被调用

虽然是在synchronized方法内,但调用wait方法时,线程会释放锁

wait/notify流程

  1. wait
  • 把当前线程放入条件队列,释放对象锁,阻塞等待,线程状态变成:waiting或者timed_waiting
  • 其他线程调用notify/notifyAll从条件队列中移除或则等待时间到,这时要重新竞争对象锁。
    • 如果能够获得对象锁,线程状态会变为Runnable,并从wait调用中返回。
    • 否则,该对象加入对象锁等待队列,线程状态变成blocked,只有获得锁之后才能从wait调用中返回。

注意:线程从wait调用返回后,不代表其等待的条件就一定成立了,它需要重新检查其等待的条件,一般的调用模式是:

调用notify会把在条件队列中等待的线程唤醒并从条件队列中移除,但它不会释放对象锁,也就是说,只有在包括notify的synchronized代码块执行完后,等待的线程才会从wait调用中返回。
他们被不同的线程调用,但共享相同的锁和条件等待队列(相同对象的synchronized代码块内),他们围绕一个共享的条件变量进行协作,这个条件变量是程序自己维护的,当条件不成立时,线程调用wait进入条件等待队列,另一个线程修改了条件变量后调用notify,调用wait的线程唤醒后需要重新检查条件变量。从多线程的角度看,他们围绕共享变量进行协作,从调用wait的线程角度看,它阻塞等待一个条件成立。我们在设计多线程协作时,需要想清楚协作的共享变量和条件是什么,这是协作的核心。

案例

生产者消费者模式

阻塞队列:

  • 接口BlockingQueue和BlockingDeque
  • 基于数组实现类:ArrayBlockingQueue
  • 基于链表的实现类:LinkedBlockingQueue和LinkedBlockingDeque
  • 基于堆的实现类:PriorityBlockingQueue

在实际开发中,应优先使用这些类。

场景

  • 同时开始
等待共享变量成立是,同时唤醒
  • 等待结束
使用join方法让主线程等待子线程结束,join实际上就是使用了wait方法,当线程 isAlive() = true ,join就一直等待下去;当线程结束时,java系统调用notifyAll来通知。
但是使用join有时比较麻烦,需要主线程逐一等待每个子线程
可以使用主线程与各个子线程协作的共享变量是一个数,这个数表示未完成的线程个数,初始值为子线程个数,主线程等待该值为0,而每个子线程结束后都将改值减一,当减为0时调用 notifyAll()
  • 异步结果
在主从模式中,手工创建线程往往比较麻烦,一种常见的模式是异步调用,异步调用返回一个一般称为Future的对象,通过它可以获得最终的结果。在java中,表示子任务的接口是Callable。

java中相关接口和类

  • 表示异步结果的接口Future和实现类FutureTask

  • 用于异步执行任务的接口Executor,以及有更多功能的子接口ExecutorService

  • 用于创建Executor和ExecutorService的工厂方法类Executors

  • 集合点

各个线程先是分头行动,各自到达一个集合点,在集合点需要集齐所有线程,交换数据,然后再进行下一步动作。怎么表示这种协作呢?协作的共享变量依然是一个数,这个数表示未到达集合点的线程个数,初始值为子线程个数,每个线程到达集合点后将该值减一,如果不为0,表示还有别的线程未到,进行等待,如果变为0,表示自己是最后一个到的,调用notifyAll 唤醒所有的线程。

2.4线程的中断

前要

首先要知道如何在java中取消或者关闭一个线程?然后是哪些场景需要取消关闭线程,再是取消/关闭的机制,以及线程对中断的反应和如何正确的取消/关闭线程。
2.4.1取消关闭的场景

start启动线程,开始执行run方法,执行完run方法,线程退出。

  1. 很多线程的运行模式是死循环,比如生产者/消费者模式中,消费者主体就是一个死循环,它不停的从队列中接受任务,执行任务,在停止程序时,我们需要以一种优雅的方式关闭该线程。
  2. 在一些图形用户界面程序中,线程是用户启动的,完成一些任务,比如从网上下载图片,在下载的过程中,用户可能会希望取消该任务。
  3. 在一些场景中,比如从第三方服务器查询一个结果,我们希望在限定的时间内得到结果,如果得不到,我们会希望取消该任务。
  4. 又时,我们会启动多个线程做同一件事,比如类似抢火车票,我们可能会让多个好友帮忙从多个渠道买火车票,只要有一个渠道买到了,我们就通知取消其他渠道。
2.4.2取消/关闭机制
在java中,停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,他是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出。

Thread中定义的关于中断的方法:

public boolean isInterrupted()
public void interrupt()
public static boolean interrupted()
isInterrupted,和 interrupt 是实例方法,调用他们需要通过线程对象;
interrupted 是静态方法,实际上会调用 Thread.currentThread() 操作当前对象。

每个线程都有一个中断标志位,表示该线程是否被中断了

  1. isInterrupted:返回对应线程的中断标志位是否为true
  2. interrupted:返回当前线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用interrupted(),第一次返回结果为 true,第二次一般就是 false (除非同时又发生了一次中断)
  3. interrupt:表示中断对应的线程。
2.4.3线程对中断的反应
interrupt() 对线程的影响与线程的状态和在进行的IO操作有关。我们主要考虑线程的状态
  • runnable:线程正在运行或具备运行条件只是在等待操作系统调度。
  • waiting/timed_waiting:线程在等待某个条件或者等待超时。
  • blocked:线程在等待锁,试图进入同步块。
  • new/terminated:线程还未启动或者已经结束。

1.runnable

如果线程在运行过程中,且没有执行io操作, interrupt()只是会设置线程的中断标志位,没有任何其他作用。线程应该在运行过程中合适的位置检查中断标志位,比如,如果主体代码是一个循环,可以在循环处进行检查。

while(!Thread.currentThread().isInterrupted()) {
  
}

2.waiting/timed_waiting

线程调用 join/wait/sleep方法会进入waiting/timed_waiting状态,在这些状态时,对线程对象调用 interrupt() 会使得线程抛出InterruptedException。抛出异常后,中断标志位会被清空,而不是设置

捕获到InterruptedException异常,通常表示希望结束该线程,线程大致有两种处理方式:

1)向上传递该异常,这使得该方法也变成了一个可中断的方法,需要调用者进行处理。

2)有些情况,不能向上传递异常,比如Thread的run方法,它的声明是固定的,不能抛出任何受检异常,这时,应该捕获异常,进行核市的清理操作,清理后,一般应该调用Thread的interrupt方法设置中断标志位,使得其他代码有办法知道它发生了中断。

3,blocked

如果线程在等待锁,对线程对象调用 interrupt()只是会设置线程的中断标志位,线程依然会处于blocked状态,也就是说:interrupt()并不能使一个在等待锁的线程真正”中断“。

在使用synchronized 关键字获取锁的过程中不响应中断请求,这时synchronized 的局限性。如果这对程序是一个问题,应该使用显式锁。

new/terminate

如果线程尚未启动(new),或者已经结束(terminate)。则调用interrupt()对它没有任何效果,中断标志位也不会被设置。

2.4.4如何正确地取消/关闭线程

interrupt方法不一定会正真”中断“线程,它只是一种协作机制,如果不明白线程在做什么,不应该贸然地调用线程的interrupt方法。以为这样就能取消线程。

对于线程提供服务的程序模块而言,它应该封装取消/关闭操作,提供单独的取消/关闭方法给掉哟个者,外部调用者应该调用这些方法,而不是直接调用interrupt。java并发库的一些代码就提供了单独的取消/关闭方法,比如,Future接口提供了如下方法以取消任务:

boolean cancel(boolean mayInterruptIfRunning);

ExecutorService

void shutdown();
List<Runnable> shutdownNow();
2.4.5小结

​ 首先:线程的取消/关闭,主要依赖中断,但它只是一种协作机制,不会强制终止线程。作为线程的实现者应该提供明确的取消/关闭方法;作为线程的调用者,应该使用其取消/关闭方法,而不是贸然调用 interrupt

3.并发包的基石

3.1原子变量和cas

原子变量:就是和原子性的变量,保证在对变量操作时的安全性,且成本较低。

常用的原子变量

  • AtomicBoolean:原子Boolean类型,常用来在程序中表示一个标志位。
  • AtomicInteger:原子Integer类型。
  • AtomicLong:原子Long类型,常用来在程序中生成唯一序列号。
  • AtomicReference:原子引用类型,用来以原子方式更新复杂类型。

之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法。

这些方法都依赖一个public方法:

public final boolean compareAndSet(int expect,int update);

compareAndSet是一个非常重要的方法,比较并设置,我们称之为CAS。有两个参数 expect,update;如果当前值等于 expect ,则更新为 update,否则就不跟新,如果更新成功,就返回 true,否则返回 false。

AtomicInteger可以在程序中做一个计数器,多个线程并发更新,也总能实现正确性。

2.基本原理和思维

AtomicInteger的主要内部成员是:

private volatile int value;

它的声明中使用了volatile,则会使必须的,以保证内存可见性。

incrementAndGet方法:

public final int incrementAntGet() {
  for(;;) {
    int current = get();
    int next = current + 1;
    if (compareAndSet(current,next))
      	return next;
  }
}
代码主体是个死循环,先获取当前值,计算期望值next,然后调用CAS方法进行更新,如果没有更新成功,说明value被别的线程改了,则再去取最新值并尝试更新直到成功为止。

与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的。它假定更新很可能冲突,所以先获取锁,得到锁才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,但使用CAS更新,也就是进行冲突检测,如果确实冲突了,那也没关系,继续尝试好了。synchronized代表一种阻塞式算法,得不到锁的时候,进入锁等待队列,等待其他线程唤醒,有上下文切换开销。对于大部分比较简单的操作,无论是在低并发还是高并发情况下,这种乐观非阻塞方式的性能都远高于悲观阻塞式方式。

原子变量相对比较简单,但对于复杂一些的数据结构和算法,非阻塞方式往往难以实现和理解。然而java并发包提供了一些非阻塞容器。

  • ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞并发队列
  • ConcurrentSkipListMap和ConcurrentSkipListSet:非阻塞并发Map和Set

compareAndSet的实现:

public final boolean compareAndSet(int expect,int update) {
  return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}

unsafe:

private static final Unsafe unsafe = Unsafe.getUnsafe();

它是Sun的私有实现,从名字上看,表示的也是”不安全“,一般应用程序不应该直接使用。原理上,一般的计算机系统都是在硬件层次上直接支持CAS命令。而java的实现都会利用这些特殊指令。从程序的角度看,可以将compareAndSet视为计算机的基础操作,直接接纳就好。

3.1.2ABA问题

使用CAS方式跟新有一个ABA问题:假设当前值为A,如果另一个线程将A修改成B,再修改回A,当前线程的CAS操作无法分辨当前值发生过变化。

​ ABA是不是一个问题与线程的逻辑有关,一般不是问题。而如果确实有问题,解决方法是使用AtomicStampedReference,在修改值的同时附加一个时间戳,只有值和时间都相同才进行修改,其CAS方法声明为:

public boolean compareAndSet(
V expectedReference,V newReference,int expectedStamp,int newStamp)
3.1.3小结

对于原子变量,以及其原理CAS,在并发环境中的计数,产生序列号等需求应该使用原子变量而非锁。CAS是java并发包的基础,基于它可以实现高效的,乐观,非阻塞式数据结构和算法,它也是并发包中锁,同步工具和各种容器的基础。

3.2显式锁

​ java并发包中的显式锁可以解决synchronized的限制。

java并发包中的显式锁接口和类位于包java.util.concurrent.locks下,主要接口和类有:

  • 锁接口 Lock,主要实现类是 ReentranlLock;
  • 读写锁接口ReadWriteLock,主要实现类是ReentrantReadWriteLock。
3.2.1接口Lock

显式锁接口Lock的定义:

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(Long time,Timeout unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

解释:

1)lock()/unlock():就是普通的获取锁和释放锁的方法,lock()会阻塞直到成功。

2)lockInterruptibly():与lock()不同的是,它可以响应中断,如果被其他线程中断了,则抛出InterruptedException。

3)tryLock():只是尝试获取锁,立即返回,不阻塞,如果获取成功,返回true,否则返回false。

4)tryLock(Long time,Timeout unit):先尝试获取锁,如果能够成功则立即返回true,否则阻塞等待,但等待的最长时间由指定的参数设置,在等待的同时响应中断,如果发生了中断,抛出InterruptedException,如果等待的时间内获取了锁,返回true,否则返回false。

5)newCondition:新建一个条件,一个Lock可以关联多个条件,关于多个条件.

3.2.2可重入锁ReentrantLock
1.基本用法

Lock接口的主要实现类是ReentrantLock

  • 可重入,一个线程在持有一个锁的同时,可以取竞争另一个锁(可持续获得锁).
  • 可以解决竞态条件问题.
  • 可以保证内存可见性.

ReentrantLock的两个构造方法:

public ReentrantLock()
public ReentrantLock(boolean fair)

​ 参数 fair 表示是否保证公平,不指定的情况下,默认false,表示不保证公平.所谓公平是指,等待时间最长的线程优先获得锁.保证公平会影响性能,一般也不需要,所以默认不保证,synchronized锁也是不保证公平的.

​ 使用显式锁,一定要记得调用unlock.一般而言,应该将lock之后的代码包装到try语句内,在finally语句内释放锁.

public void incr() {
  lock.lock();
  try {
    count ++;
  } finally {
    lock.unlock();
  }
}
2.使用tryLock避免死锁

​ 使用tryLock(),可以避免死锁.

在持有一个锁获取另一个锁而获取不到的时候,可以是释放已持有的锁,给其他线程获取锁的机会,然后重试获取所有锁。

3.2.3ReentrantLock的实现管理

在最底层它依赖CAS方法,另外,它依赖与类LockSupport中的一些方法

1.LockSupport

基本方法:

public static void park();
public static void parkNanos(Long nanos);
public static void parkUntil(Long deadline);
public static void unpark(Thread thread);

​ park 使得当前线程放弃cpu,进入等待状态(waiting),操作系统不再对它进行调度;当有其他线程对它调用了 unpark,unpark 使指定参数的线程恢复可运行状态。

public static void main(String [] main) throws InterruptedException {
  Thread t = new Thread(){
     public void run() {
    LockSupport.park(); //放弃cpu
    System.out.println("exit");
  	}
  };
  t.start(); //启动子线程
  Thread.sleep(1000); //睡眠1秒确保子线程先运行到放弃cpu
  LockSupport.unpark(t);
};

park 不同于Thread.yield(),yield只是告诉操作系统可以先让其他线程运行,但自己依然是可以运行状态,而park会放弃调度资格,使线程进入waiting状态。

park是响应中断的,当有中断发生时,park会返回,线程的中断状态会被设置。另外还需要说明的是,park可能会无缘无故有地返回,程序应该检查park等待条件是否满足。

park有两个变体:

  • parkNanos:可以指定等待的最长时间,参数是相对与当前时间的纳秒数。
  • parkUntil:可以指定最长等待时间,参数是绝对时间,是相对于纪元时的毫秒数。

当等待超时的时候,它们也会返回。

这些park方法还有一些变体,可以指定一个对象,表示是由该对象而进行等待的,以便于调试,通常传递的值是this。

public static void park(Object blocker);

LockSupport有一个方法,可以返回一个线程的blocker对象:

public static Object getBlocker(Thread t);

​ 这些park/unpark方法是怎么实现的,与CAS一样,他们也调用了Unsafe类中的对应方法。

Unsafe类最终调用了操作系统的API,从程序员的角度,我们可以认为Lock—Support中的这些方法就是基本操作。

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