java 多线程

一、快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount != expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。

场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二、安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。

<p>缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

java线程池ThreadPoolExecutor类使用详解 - bigfan - 博客园 (cnblogs.com)

线程与进程:

(5条消息) 一文彻底搞懂并发容器(ConcurrentHashMap、BlockingQueue等)_日有寸进-CSDN博客

区别:

1、进程拥有一整套自己的变量,而进程之间可以共享数据。

2、线程属于更轻量级,开销比进程小。

InterruptedException:

若是我们调用线程的中断方法,当程序即将进入或是已经进入阻塞调用的时候,那么这个中断信号应该由InterruptedException捕获并进行重置;当run()方

法程序段中不会出现阻塞操作的时候,这时候中断并不会抛出异常,我们需要通过interrupted()方法进行中断检查和中断标志的重置。另外,知道IO操作

和synchronized上的阻塞不可中断也是必要的。

一般来说中断线程分为三种情况:

(一) :中断非阻塞线程

(二):中断阻塞线程

(三):不可中断线程:阻塞的IO线程不能中断,关闭资源可以中断。尽量使用NIO保证可以中断。

https://www.jianshu.com/p/a8abe097d4ed

线程状态:

new 新建状态,还没有调用start方法时;

runnable:可运行状态,处于执行中的状态,可能会在等待资源。

blocked:被阻塞,线程处于阻塞状态,线程在等待进入某一个同步代码块,但未获得锁的状态。

waitting:等待状态。调用了waitjoinpark方法后的等待状态;

timed-waitting:计时等待,调用了

         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>

这些方法的等待状态。

terminated:终止状态,线程结束执行。

线程属性:

通过setpriority设置优先级,1-10优先级由低到高。

依赖于操作系统,不要将程序依赖于优先级。

守护线程:

为其余线程提供服务,其余线程都退出后,它就退出了。

未捕获异常:

unCaguthException

同步:

为了解决竞争问题,可以有以下方案:

1、使用Lock类

lock()方法会锁住代码块,其余线程将被阻塞,无法访问。这种方式一定要unlock,否则线程可能永远阻塞。

如果使用锁, 就不能使用带资源的 try 语句。首先, 解锁方法名不是 close。不过, 即使将它重命名, 带资源的 try 语句也无法正常工作。它的首部希望声明一个新变量。但 是如果使用一个锁, 你可能想使用多个线程共享的那个变量(而不是新变量)。

private Lock bankLock = new ReentrantLock0;// ReentrantLock implements the Lock interface

可重入锁》因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( hold count) 来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。 由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。

公平锁、非公平锁:公平锁倾向于选择等待时间最长的线程,但性能较低,同时也不是绝对公平的。

临界区:

await与wait:

条件对象、条件变量:Condition可用于判断是否让出锁,可以调用await或wait,同时使用signalsignallAll来唤醒。

{
private final double□ accounts;
private Lock bankLock ;
private Condition sufficientFunds;

public Bank(int n, double initialBalance)
t
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLockO;
sufficientFunds = bankLock.newCondition0;

2、sychronized

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下, 并不需要那样的控制,并且可以使用一种嵌人到 Java语言内部的机制。从 1.0 版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized关键字声明,那么对象的锁 将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

换句话说,
public synchronized void methodO
{
method body
}
等价于
public void methodQ
{
this.intrinsidock.1ock();
try
{
method body
}
finally { this.intrinsicLock.unlockO; }
}

wait、notifyAll 以及 notify 方法是 Object 类的 final 方法。Condition 方法必须被命 名为 await、signalAll 和 signal 以便它们不会与那些方法发生冲突。

每一个对象有一个内部锁, 并且该锁有一个内部条件。由锁来管理那些试图进入 synchronized 方法的线程,由条件来管理那些调用 wait 的线程。

将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关的类对 象的内部锁。例如,如果 Bank 类有一个静态同步的方法,那么当该方法被调用时,Bankxlass 对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。

 同步阻塞:

还有 另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:

synchronized (obj) // this is the syntax for a synchronized block

{

  critical section

}

于是它获得 Obj 的锁。

不要视图获取同步阻塞对象的锁来保证同步。

监视器:

 多年来,研 究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程 的安全性。最成功的解决方案之一是监视器(monitor), 这一概念最早是由 PerBrinchHansen 和 Tony Hoare 在 20 世纪 70 年代提出的。用 Java 的术语来讲,监视器具有如下特性:

•监视器是只包含私有域的类。

•每个监视器类的对象有一个相关的锁。

•使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj.meth0d(), 那 么 obj 对象的锁是在方法调用开始时自动获得, 并且当方法返回时自动释放该锁。因为所有 的域是私有的,这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问 该域。

•该锁可以有任意多个相关条件。

Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的 锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监 视器方法。通过调用 wait/notifyAU/notify 来访问条件变量。 然而, 在下述的 3 个方面 Java 对象不同于监视器, 从而使得线程的安全性下降:

•域不要求必须是 private。

•方法不要求必须是 synchronized。

•内部锁对客户是可用的。

Volatile:

有时,仅仅为了读写一个或两个实例域就使用同步, 显得开销过大了。毕竟,什么地方 能出错呢? 遗憾的是, 使用现代的处理器与编译器, 出错的可能性很大。 •多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是, 运行在不同处理器上的线程可能在同一个内存位置取到不同的值。 •编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码 语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而, 内存的值可以被另一个线程改变! 如果你使用锁来保护可以被多个线程访问的代码,那么可以不考虑这种问题。

编译 器被要求通过在必要的时候刷新本地缓存来保持锁的效应,

并且不能不正当地重新排序 指令。

Volatile 变量不能提供原子性。

例如, 方法

public void flipDoneO

{ done = !done; } // not atomic

不能确保翻转域中的值。不能保证读取、 翻转和写入不被中断。

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatileo

原子性:

指某一操作是线程安全的,不会被终断,concurrent包中有许多包含原子操作的类。

死锁:

两个以上线程同时阻塞。

线程局部变量:

使用 ThreadLocal 辅助类为各个线程提供各自的实例。各个线程间不会相互干扰。

锁测试与超时:

。tryLock 方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回 false, 而且线程可以立即离开去做其他事情。

可以调用 tryLock 时,使用超时参数,像这样: if (myLock.tryLock(100, TineUnit.MILLISECONDS)) . . .

TimeUnit 是一 枚举类型,可以取的值包括 SECONDS、MILLISECONDS, MICROSECONDS 和 NANOSECONDS。 lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之 前一直处于阻塞状态。如果出现死锁, 那么,lock 方法就无法终止。

然而, 如果调用带有用超时参数的 tryLock, 那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。

也可以调用 locklnterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法

在等待一个条件时, 也可以提供一个超时: myCondition.await(100, TineUniBILLISECONDS))

如果一个线程被另一个线程通过调用 signalAU 或 signal 激活, 或者超时时限已达到,或 者线程被中断, 那么 await 方法将返回。

如果等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你希望出 现这种情况时线程继续等待(可能不太合理,) 可以使用 awaitUninterruptibly 方法代替 await。

读写锁:

ReentrantReadWriteLock,可以给读方法加readlock.写方法加writeLock。

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private Lock read = rwl.readLock();
    private Lock write = rwl.writeLock();
    private int start = 15;
    public void setStart(int x) {
        write.lock();
        try{
            this.start = x;
        }catch(Exception e) {
            
        }finally {
            write.unlock();
        }
    }
    public int getStart() {
        read.lock();
        try{
            return start;
        }finally {
            read.unlock();
        }
    }

为什么弃用stop和suspend方法:

当线程被终止,立 即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如’ 假定 TransferThread 在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银 行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。 当线程要终止另一个线程时, 无法知道什么时候调用 stop 方法是安全的, 什么时候导致 对象被破坏。

被停止的线程通过抛出 ThreadDeath 异常退出 所有它所调用的同步方法。结果是,该线程释放它持有的内部对象锁

与 stop 不同,suspend 不会破坏对象。但是, 如果用 suspend 挂起一个持有一个锁的线程, 那么,该锁在恢复之前是不可用的。如果调用 suspend 方法的线程试图获得同一个锁, 那么程序死锁: 被挂起的线程等着被恢复,而将其 挂起的线程等待获得锁。

阻塞队列:

对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生 产者线程向队列插人元素, 消费者线程则取出它们。使用队列,可以安全地从一个线程向另 一个线程传递数据。

LinkedBlockingQueue 的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端 的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需 要公平性。若设置了公平参数, 则那么等待了最长时间的线程会优先得到处理。通常,公平 性会降低性能,只有在确实非常需要时才使用它。 PriorityBlockingQueue 是一个带优先级的队列, 而不是先进先出队列。元素按照它们的 优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的, 取元素的操作会阻 塞。

不需要显式的线程同步。在这个应用程序中, 我们使用队列数据结构作为一种同 步机制。

线程安全的集合:

java.util.concurrent 包提供了映射、 有序集和队列的高效实现:ConcurrentHashMap、 ConcurrentSkipListMap > ConcurrentSkipListSet 和 ConcurrentLinkedQueue。 这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。 与大多数集合不同,size 方法不必在常量时间内操作。确定这样的集合当前的大小通常 需要遍历。

集合返回弱一致性( weakly consistent) 的迭代器。这意味着迭代器不一定能反映出它 们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会拋出 Concurrent ModificationException 异常。(大于20亿的条目怎么处理)java.util 包中的迭代器 将抛出一个 ConcurrentModificationException 异常。

 对于一个包含超过 20 亿条目的映射该如何 处理? JavaSE 8 引入了一个 mappingCount 方法可以把大小作为 long 返回

ConcurrentHashMap:

并发视图集:

Set words = ConcurrentHashMap.newKeySet();

较早的线程安全集合:

Vector 和 Hashtable 类就提供了线程安全的动态数组和散列表的 实现。现在这些类被弃用了, 取而代之的是 AnayList 和 HashMap 类。这些类不是线程安全 的,而集合库中提供了不同的机制。任何集合类都可以通过使用同步包装器(synchronization wrapper) 变成线程安全的:

List synchArrayList = Col lections,synchronizedList(new ArrayList());

Map synchHashMap = Col1ections.synchroni zedMap(new HashMap0);

结果集合的方法使用锁加以保护,提供了线程安全访问

如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“ 客户端” 锁定:

synchronized (synchHashMap) {

Iterator iter = synchHashMap.keySet().iterator();

while (iter.hasNextO) . . }

如果使用“ foreach” 循环必须使用同样的代码, 因为循环使用了迭代器。

最好使用 java.Util.COnciirrent 包中定义的集合, 不使用同步包装器中的。特别是, 假如它 们访问的是不同的桶, 由于 ConcurrentHashMap 已经精心地实现了,多线程可以访问它而且 不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的 ArrayList 可 以胜过 CopyOnWriteArrayList 。

Callable与Future:

Callable 与 Runnable 类似,但是有返回值。Callable 接口是一个参数化的类型, 只有一 个方法 call。

public interface Ca11able {

V cal() throws Exception;

}

类型参数是返回值的类型。例如, Callable 表示一个最终返回 Integer 对象的异 步计算。

public class CallableTest {
    
    public static void main(String[] args) {
        try(Scanner in = new Scanner(System.in)){
            System.out.println("enter directory");
            String directory = in.nextLine();
            System.out.println("enter key word");
            String keyWord  = in.nextLine();
            MatcherCounter counter = new MatcherCounter(new File(directory), keyWord);
            FutureTask<Integer> task = new FutureTask<>(counter);
            
            Thread t = new Thread(task);
            t.start();
            try {
                System.out.println(task.get());
            } catch (InterruptedException | ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

}

class MatcherCounter implements Callable<Integer>{
    
    private File directory;
    private String keyWord;
    
    public MatcherCounter(File directory,String keyWord) {
        this.directory = directory;
        this.keyWord = keyWord;
    }
    
    @Override
    public Integer call() {
        
        int count = 0;
        try {
            File[] files = directory.listFiles();
            List<Future<Integer>> results = new ArrayList<>();
            for(File file:files) {
                if(file.isDirectory()) {
                    MatcherCounter counter = new MatcherCounter(file, keyWord);
                    FutureTask<Integer> task = new FutureTask<>(counter);
                    results.add(task);
                    Thread t = new Thread(task);
                    t.start();
                }else {
                    if(search(file)) {
                        count++;
                    }
                }
            }
            
            for(Future<Integer> result: results) {
                try {
                    count += result.get();
                }catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
        
        return count;
    }
    
    public boolean search(File file) {
        try {
            try(Scanner in = new Scanner(file,"UTF-8")) {
                boolean found = false;
                while(!found&& in.hasNextLine()) {
                    String line = in.nextLine();
                    found = line.contains(keyWord);
                }
                return found;
            }
        }catch(Exception e) {
            return false;
        }
    }
    
}

执行器:

 表 14-2 执行者工厂方法

方 法 描 述

newCachedThreadPool 必要时创建新线程;空闲线程会被保留 60 秒

newFixedThreadPool 该池包含固定数量的线程;空闲线程会一直被保留

newSingleThreadExecutor 只有一个线程的 “ 池”, 该线程顺序执行每一个提交的任务(类似于 Swing 事件分配线程)

newScheduledThreadPool 用于预定执行而构建的固定线程池, 替代 java.util.Timer

newSingleThreadScheduledExecutor 用于预定执行而构建的单线程 “ 池”

线程池:

Fork-Join 框架:

Java SE 7中新引入了 fork-join 框架,专门用来 支持后一类应用。假设有一个处理任务, 它可以很自然地分解为子任务,

class Counter extends RecursiveTask<Integer>
{
protected Integer compute()
{
if (to - fro« < THRESHOLD)
{
solve problem directly
}
else
{
int mid = (from + to) / 2;
Counter first = new Counter(va1ues, from, mid, filter);
Counter second = new Counter(va1ues, mid, to, filter);
i nvokeAll (fi rst, second):
return first.joinO + second.joinO;
}
}
}

在后台, fork-join 框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方 法称为工作密取(work stealing)。每个工作线程都有一个双端队列 ( deque ) 来完成任务。一 个工作线程将子任务压人其双端队列的队头。(只有一个线程可以访问队头,所以不需要加 锁。)一个工作线程空闲时,它会从另一个双端队列的队尾“ 密取” 一个任务。由于大的子任 务都在队尾, 这种密取很少出现。

原文地址:https://www.cnblogs.com/baldprogrammer/p/13690671.html