码迷,mamicode.com
首页 > 编程语言 > 详细

多线程

时间:2016-07-13 17:33:15      阅读:218      评论:0      收藏:0      [点我收藏+]

标签:

1 概述

2 Runnable

为了实现多线程共享同一段代码,一般将共享代码放在runnable中执行,然后new一个runnable出来,给3个thread共享运行。

3 interrupted

可以在 Thread 对象上调用 isInterrupted()方法来检查任何线程的中断状态。

这里需要注意:线程一旦被中断,isInterrupted()方法便会返回 true,而一旦 sleep()方法抛出异常,它将清空中断标志,此时isInterrupted()方法将返回 false

可以使用 Thread.interrupted()静态方法来检查当前线程的中断状态(并隐式重置为 false)。又由于它是静态方法,因此不能在特定的线程上使用,而只能报告调用它的线程的中断状态,如果线程被中断,而且中断状态尚不清楚,那么,这个方法返回 true。与 isInterrupted()不同,它将自动重置中断状态为 false,第二次调用 Thread.interrupted()方法,总是返回 false,除非中断了线程。

4 yield

yield 可以直接用 Thread 类调用,yield 让出 CPU 执行权给同等级的线程,如果没有相同级别的线程在等待 CPU 的执行权,则该线程继续执行。

5 join

join 方法用线程对象调用,如果在一个线程 A 中调用另一个线程 B 的 join 方法,线程 A 将会等待线程 B 执行完毕后再执行。

6 守护线程

setDaemon(true)来设置当前线程为守护线程

  1. setDaemon(true)必须在调用线程的 start()方法之前设置,否则会抛出IllegalThreadStateException 异常。
  2. 在守护线程中产生的新线程也是守护线程。
  3. 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

7 线程阻塞

  1. 当线程执行 Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;
  2. 当线程碰到一条 wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)
  3. 线程阻塞与不同 I/O 的方式有多种。常见的一种方式是 InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间,此阻塞是无法中断的
  4. 线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得 synchronized 语句必须的锁时阻塞),此阻塞也是无法中断的

在 wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

8 volatile

Volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

当且仅当满足以下所有条件时,才应该使用 volatile 变量

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  2. 该变量没有包含在具有其他变量的不变式中。

9 synchronized

采用 synchronized 修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个 monitor (锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池等待。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

  1. 一个方法内有多个线程时,每个线程都有自己的局部变量拷贝
  2. 类的每个实例都有自己的对象级别锁。
  3. 访问同一类中不同实例对象中的同步代码块不存在阻塞。
  4. 持有一个对象级别的锁不会阻止该线程被交换出来,但是不会释放锁。只有运行完同步块才会释放锁。
  5. 持有一个对象级别的锁不会阻塞其他线程访问这一对象的非synchronized代码。
  6. 持有对象级别的锁的线程会让其他线程阻塞在该对象内所有的synchronized代码外。
  7. 使用synchronized(obj)的同步块,可以获得obj上的对象级别锁。
  8. 类级别锁被特定类的所有示例共享,它用于控制对 static 成员变量以及 static 方法的并发访问。

synchronized 关键字经过编译后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加 1,相应地,在执行 monitorexit 指令时会将锁计数器减 1,当计数器为 0 时,锁便被释放了。由于 synchronized 同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。

10 集合相关

Vector和HashTable是多线程安全的

在 Collections 类中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合:

public static Collection synchronizedCollention(Collection c)

public static List synchronizedList(list l)

public static Map synchronizedMap(Map m)

public static Set synchronizedSet(Set s)

public static SortedMap synchronizedSortedMap(SortedMap sm)

public static SortedSet synchronizedSortedSet(SortedSet ss)

这样封装出来的对象就是同步的集合对象,当然也可以继续操作原对象以进行非同步操作。

在 Java 语言中,大部分的线程安全类都是相对线程安全的,它能保证对这个对象单独的操作时线程安全的,我们在调用的时候不需要额外的保障,但是对于一些特定的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。例如 Vector、HashTable、Collections的synchronizedXxxx()方法包装的集合等。

10 集合的安全Iterator遍历

添加了Collections的synchronizedXxxx()封装,可以解决增加和删除列表的时候出现的同步问题,但是对于使用Iterator遍历,还是会抛出异常java.util.ConcurrentModificationException

要解决以上异常,首先是要使用Collections的synchronizedXxxx()方法包装的集合,然后使用synchronized(list)包裹Iterator的遍历代码。其中list为需要遍历的集合的的包装类。

public class TestListIterator2 {

    public static void main(String[] args) {

        List<String> list = Collections.synchronizedList(new ArrayList<>());
        new Thread(new Runnable() {
            int index = 1;
            @Override
            public void run() {
                while (true) {
                    list.add("this is " + (index++));
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                     //不加synchronized这句会异常,加了就正常了。
                     //但是加了有个问题,就是遍历时会阻塞列表的增加和删除操作
                    synchronized (list) { 
                        Iterator<String> iterator = list.iterator();
                        while (iterator.hasNext()) {
                            System.out.println(iterator.next());
                        }
                    }
                }
            }
        }).start(); 
    }
}

11 死锁

死锁发生的情况:线程t1持有对象obj1的锁,等待获取对象obj2的锁,同时线程t2持有对象obj2的锁,等待获取obj1的锁。例如如下代码必然产生死锁。

public class TestDeadLock {
    public synchronized void func1(TestDeadLock lock) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (lock != null) {
            System.out.println("in " + Thread.currentThread().getId());
            lock.func1(null);
            System.out.println("out " + Thread.currentThread().getId());
        }
    }

    public static void main(String[] args) {
        TestDeadLock lock = new TestDeadLock();
        TestDeadLock lock2 = new TestDeadLock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.func1(lock2);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock2.func1(lock);
            }
        }).start();
    }
}

12 可重入内置锁

java的锁都是可重入的。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为 0 时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为 1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为 0 时,这个锁将被释放。

class TestReinLock extends SuperTestReInLock {
    public synchronized void doSth() {
        System.out.println("sun");
        super.doSth();
        System.out.println("finsh");
    }

    public static void main(String[] args) {
        TestReinLock testReinLock = new TestReinLock();
        testReinLock.doSth();
    }

}

class SuperTestReInLock {
    public synchronized void doSth() {
        System.out.println("super");
    }
}

由于 SuperTestReInLock 和 TestReinLock 中的 doSth 方法都是 synchronized 方法,因此每个 doSth 方法在执行前都会获取 TestReinLock 对象实例上的锁。如果内置锁不是可重入的,那么在调用 super.doSth 时将无法获得该 TestReinLock 对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。

13 wait

public final void wait() throws InterruptedException,IllegalMonitorStateException

在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。

14 notify和notifyAll

public final native void notify() throws IllegalMonitorStateException

该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁.

该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait.

notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

注意

  1. notify如果在wait之前执行的,就可能造成wait遗漏,而导致线程一直等待下去。
  2. 如果wait的条件还不满足,而notifyAll已经发出来,则会导致线程提前接到通知。比如两个线程等待删除一个为空的列表,而添加了一个元素,notifyAll的时候,线程都会唤醒,然后一个线程先获取锁,删除成功,另一个再获取锁,删除就失败了。此时可以考虑在线程使用while循环,每次获取锁之前都判断是否需要等待。
  3. 在 wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

15 happen-before规则

Java 语言中有一个“先行发生”(happen—before)的规则,它是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,其意思就是说,在发生操作 B 之前,操作A产生的影响都能被操作 B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

java中happen-before(时间上的先后顺序)规则如下:

  1. 程序次序规则:在单独线程中,按代码的执行流顺序,先执行的操作 happen-before 后执行的操作。
  2. 管理锁定规则:对同一个锁的 lock 操作 happen—before 这个锁的 unlock 操作。
  3. volatile变量规则:对一个 volatile 变量的写操作 happen—before 后面对该变量的读操作。
  4. 线程启动规则:Thread 对象的 start()方法 happen—before 此线程的每一个动作。
  5. 线程终止规则:线程的所有操作都 happen—before 对此线程的终止检测,可以通过 Thread.join()方法结束 Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程 interrupt()方法的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before 它的 finalize()方法的开始。
  8. 传递性:如果操作 A happen—before 操作 B,操作 B happen—before 操作 C,那么可以得出 A happen—before 操作 C。

这里顺便提一下单例模式的一个bug(也就是DCL问题)

public class TestHappenBefore {
    private int cont;

    private static TestHappenBefore instance;

    private TestHappenBefore() {
        this.cont = new Random().nextInt(200)+1;
    }

    private static TestHappenBefore getInstance() {
        if (instance == null) {
            synchronized (TestHappenBefore.class) {
                if (instance == null) {
                    instance = new TestHappenBefore();
                }
            }
        }
        return instance;
    }

    public int getCont() {
        return this.cont;
    }

    public static void main(String[] args) {
        System.out.println(TestHappenBefore.getInstance().getCont());
    }
}

这里得到单一的 instance 实例是没有问题的,问题的关键在于尽管得到了 Singleton 的正确引用,但是却有可能访问到其成员变量的不正确值。具体来说 Singleton.getInstance(). getCont() 有可能返回 cont 的默认值 0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的 someField 的值不可能为 0。也就是说,构造方法和getCont方法并不存在happen-before关系。

假设线程Ⅰ是初次调用 getInstance()方法,紧接着线程Ⅱ也调用了 getInstance()方法和 getSomeField()方法.此时就有可能获取到的值为0。

对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。

而对于以上问题,有以下思路可以解决:

public class Singleton {
    private Singleton() {}  
 
    // Lazy initialization holder class idiom for static fields  
    private static class InstanceHolder {  
        private static final Singleton instance = new Singleton();  
    }  
 
    public static Singleton getSingleton() {   
        return InstanceHolder.instance;   
    }  
}

或者添加volatile关键字屏蔽指令重排序

private volatile static Singleton instance;

16 Executor

Executor 框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable 等。关于Executor的内容,请查看我的另一篇博客Executor线程池解析

17 Lock锁

Lock接口有 3 个实现它的类:ReentrantLock(重入锁)、ReetrantReadWriteLock.ReadLock(读锁)和ReetrantReadWriteLock.WriteLock(写锁)

Lock必须被显式地创建、锁定和释放。为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁。尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务

//默认使用非公平锁,如果要使用公平锁,需要传入参数true  
//公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待
Lock lock = new ReentrantLock();

........  

lock.lock();  
try {  
   //更新对象的状态  
   //捕获异常,必要时恢复到原来的不变约束  
   //如果有return语句,放在这里
}
finally {  
   lock.unlock();        //锁必须在finally块中释放
}

Lock锁部分详细内容请查看另一篇博客并发新特效之lock

18 原子变量

Java 5 中引入了注入 AutomicInteger、AutomicLong、AutomicReference 等特殊的原子性变量类,它们提供的如:compareAndSet()、incrementAndSet()和getAndIncrement()等方法都使用了 CAS 操作。因此,它们都是由硬件指令来保证的原子方法。

19 阻塞队列 BlockingQueue

java.util.concurrent.BlockingQueue接口有多个实现类:ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue 等

BlockingQueue<String> bQueue = new ArrayBlockingQueue<>(20);
for (int i = 0; i < 30; i++) {
    if (bQueue.size() == 20) {
        System.out.println("remove " + bQueue.take());
    }
    bQueue.put("add" + i);
    System.out.println("add " + i);
}

20 阻塞栈 BlockingDeque

阻塞栈的接口java.util.concurrent.BlockingDeque也有很多实现类,比如LinkedBlockingDeque等。用法类似

21 障碍器 CyclicBarrier

CyclicBarrier,它适用于这样一种情况:你希望创建一组任务,它们并发地执行工作,另外的一个任务在这一组任务并发执行结束前一直阻塞等待,直到该组任务全部执行结束,这个任务才得以执行。这非常像 CountDownLatch,只是 CountDownLatch 是只触发一次的事件,而 CyclicBarrier 可以多次重用。

22 信号量 Semaphore

Java 并发包中的信号量 Semaphore 实际上是一个功能完毕的计数信号量,从概念上讲,它维护了一个许可集合,对控制一定资源的消费与回收有着很重要的意义。Semaphore 可以控制某个资源被同时访问的任务数,它通过acquire()获取一个许可,release()释放一个许可。如果被同时访问的任务数已满,则其他 acquire 的任务进入等待状态,直到有一个任务被 release 掉,它才能得到许可。

Semaphore 仅仅是对资源的并发访问的任务数进行监控,而不会保证线程安全

public class TestSemaphore {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(5); // 只允许5个访问

        for (int i = 0; i < 10; i++) {
            final int num = i;
            Runnable runnable = new Runnable() {

                @Override
                public void run() {
                    try {
                        semaphore.acquire();// 获取许可
                        System.out.println(Thread.currentThread().getName() + " 获取许可 " + num);

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

                        }
                        // 释放许可
                        semaphore.release();
                        System.out.println(Thread.currentThread().getName() + " 释放许可 " + num);

                        System.out.println("允许进入的任务个数: " + semaphore.availablePermits());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

            };
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}

多线程

标签:

原文地址:http://blog.csdn.net/felix_wangq/article/details/51883150

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!