Java核心技术(第8版) – 读书笔记 – 第14章

本章主要是多线程。

记得第7版的这一章貌似是卷II的。。。

1、线程:一个进程同时执行多个任务。

2、进程与线程的主要区别:每个线程之间共享数据,每个进程则拥有独立的数据空间。

3、睡眠当前线程:

java.lang.Thread.sleep(long millis);

4、启动一个新线程的步骤:
(1)实现Runnable接口,以及其中的run方法(包含线程要执行的业务逻辑)。
(2)创建上述Runnable对象,并放入Thread中:Thread t = new Thread(new XXRunable())
(3) 启动线程:t.start()
需要睡眠的时候可以sleep,如3中所述,sleep时可能产生InterruptedException异常。

5、也可以通过继承Thread的方式实现线程,但显然更推荐Runnable的方法。因为Java中是没有多继承的(而实现多个接口是可以的)。

6、不要直接调用Runnable的run方法,一定要放到Thread中。否则就变成单线程了。。。下面是多线程一个基本的例子,开启新线程用于打印,主线程等待System.in,当输入q时,结束子线程并退出。

import java.util.*;
public class MT
{
    public static void main(String [] args)
    {
        Thread t = new Thread(new Runnable()
                {
                    public void run()
                    {
                        int i=0;
                        while(true)
                        {
                            System.out.println(i++);
                            try
                            {   

                                Thread.sleep(100);
                            }
                            catch(InterruptedException e)
                            {
                                break;
                            }
                        }   

                    }
                });
        t.start();
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext())
        {
            String tok = scan.next();
            if(tok.equals("q"))
            {
                break;
            }
        }
        // end thread
        t.interrupt();
    }   

}

7、Java已经不建议使用stop等强制性方法来停止线程的执行,目前主要有两种方式:

(1)内部维护:同步一个内部变量决定是否退出:

public void run()
{
    while(bRun)
    {
        ....
    }
}

(2)外部可以通过调用Thread的interrupt()来“打扰”线程,但是线程什么时候被“打扰醒”是不一定的,有两种情况:

(a)线程一直在用CPU,没有任何中断产生,那么最好在run里的while循环自己检查一下:

while(!Thread.currentThread().isInterrupted() && ...)
{
    //normal work
}
//after main thread call Thread.inpterrupt(), would go here

(b)线程正在阻塞(产生了中断,如I/O、sleep,调度等),此时会抛出InterruptedException,因此要捕获:

public void run()
{
    try
    {
        //......
    }
    catch(Exception e)
    {
        // thread was interrupted, usually exit current thread
    }
}

8、Thread有两个类似方法:
(1)interupted():类方法,会清楚打扰位。
(2)isInterrupted():实例(对象) 方法,不会清楚打扰位。

9、线程的6种状态:
(1)New(新生):new Thread(xxx)
(2)Runnable(可运行):t.start()
(3) Blocked(被阻塞):不运行任何代码,等待再次被激活。条件:锁阻塞(获得锁、I/O等待)等
(4)Waiting(等待) :join
(5)Timed wating(计时等待) :wait、await等
(6)Terminated(被终止) :run自然运行完毕或者interupted等导致内部判断后结束。

10、Java的线程也有优先级,可以设置为1~10,越大越优先。然而,这个优先级如何调度,取决于操作系统,因此不要让程序的多线程依赖于优先级。

public class Thread
{
    // ....
    public final void setPriority(int newPriority);
    // ...
}

11、守护线程,可以直接用Thread实现:

t.setDaemon(true);

必须在.start()之前调用。

如果还有一个或以上的非守护线程,则Java进程就不会退出。因此不可以把业务逻辑放在守护线程中。当一个线程无需JVM等待它结束时(它的操作可有可无),就把它设置为守护线程吧,如定时的清空缓存等。

12、线程中一旦发生未捕获的异常,就会退出,因此要注意。除了直接catch Exception拦截所有异常外。JDK5开始,可以为每个线程设立自己的异常处理器:UncaughtExceptionHandler:

public class Thread
{
    // ...
    public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler();
}

被这个Handler处理后(前),线程还是会停止的

13、竞争条件(race condiction)的例子:两个线程同时访问并修改一个对象。

14、还是经典的银行转帐例子:初始每个账户都是50块钱,每个线程都随机从一个账户转帐随机金额到随机另外一个账户,但是经过一段时间后,所有账户的总钱数却少了:

import java.util.*;
public class Bank
{
	public Bank(int n)
	{
		accounts = new int[n];
		for(int i=0; i< accounts.length; i++)
		{
			accounts[i] = 50;
		}
	}

	public void transfer(int from ,int to, int amount)
	{
		if(accounts[from]<amount)
		{
			return;
		}
		accounts[from] -= amount;
		System.out.println("Transfer $"+amount+" from "+from+" to "+to);
		accounts[to] += amount;
		System.out.println("Total amount "+getTotalBalance());
	}

	public int getAccountsCount()
	{
		return accounts.length;
	}

	public int getTotalBalance()
	{
		int sum = 0;
		for(int account: accounts)
		{
			sum+=account;
		}
		return sum;
	}

	private int [] accounts;

	public static void main(String [] args)
	{
		Bank bank = new Bank(50);
		int thds = 10;
		Thread [] threads = new Thread[thds];
		for(int i=0; i< 10; i++)
		{
			threads[i]	= new Thread(new TransferRun(bank));
			threads[i].start();
		}
	}
}

class TransferRun implements Runnable
{

	TransferRun(Bank bank)
	{
		this.bank = bank;
	}

	public void run()
	{
		while(true)
		{
			int from = rand.nextInt(bank.getAccountsCount());
			int to = rand.nextInt(bank.getAccountsCount());
			int amount = rand.nextInt(60);
			bank.transfer(from, to, amount);
		}
	}

	private Bank bank;
	private Random rand = new Random();
}

15、上述现象理论上是随机出现的,主要原因是+=、-=不是原子操作。书上636的那个图很好的说明了这个问题。

16、JDK5开始,提供了锁对象,与sychronized关键字类似,但更加OO:

//May be field of class
ReetrantLock mylock = new ReetrantLock();

mylock.lock();
try
{
    // do sth may create race condiction
}
catch()
{
    lock.unlock();
}

一定要确保释放锁,因此放在finally里最省心。

加了这个锁之后,就可以保证同一时间,只能有一个线程进入try内的关键区。

下面是加了锁之后的代码,不管再怎么运行,总额都是2500了。

import java.util.*;
import java.util.concurrent.locks.*;
public class Bank
{
	public Bank(int n)
	{
		accounts = new int[n];
		for(int i=0; i< accounts.length; i++)
		{
			accounts[i] = 50;
		}
	}

	public void transfer(int from ,int to, int amount)
	{
		mylock.lock();
		try
		{
			if(accounts[from]<amount)
			{
				return;
			}
			accounts[from] -= amount;
			System.out.println("Transfer $"+amount+" from "+from+" to "+to);
			accounts[to] += amount;
			System.out.println("Total amount "+getTotalBalance());
		}
		finally
		{
			mylock.unlock();
		}
	}

	public int getAccountsCount()
	{
		return accounts.length;
	}

	public int getTotalBalance()
	{
		int sum = 0;
		for(int account: accounts)
		{
			sum+=account;
		}
		return sum;
	}

	private int [] accounts;
	private ReentrantLock mylock = new ReentrantLock();

	public static void main(String [] args)
	{
		Bank bank = new Bank(50);
		int thds = 10;
		Thread [] threads = new Thread[thds];
		for(int i=0; i< 10; i++)
		{
			threads[i]	= new Thread(new TransferRun(bank));
			threads[i].start();
		}
	}
}

class TransferRun implements Runnable
{

	TransferRun(Bank bank)
	{
		this.bank = bank;
	}

	public void run()
	{
		while(true)
		{
			int from = rand.nextInt(bank.getAccountsCount());
			int to = rand.nextInt(bank.getAccountsCount());
			int amount = rand.nextInt(60);
			bank.transfer(from, to, amount);
		}
	}

	private Bank bank;
	private Random rand = new Random();
}

17、ReentrantLock是可重入的(可嵌套的)。

18、有的时候,虽然进入了临界区,却发现还是必须满足某些条件才能继续,这种叫做条件对象或者条件变量(condictional variable)

19、当使用条件变量时,主要的流程如下:

(1)在ReentrantLock上创建新的条件,这相当于锁下更细粒度上的“子条件锁”,需要注意的是,虽然叫“条件”锁,但它不包含任何的条件判断逻辑。

Condition cond= ReentrantLock.newCondition();

(2)在条件判断时,构造如下的结构:

while(!condiction_is_achieve)
{
    cond.await();
}
//here is condiction already achieveed

在上述结构下,一旦cond.await(),当前线程就被阻塞,它将只能由其他线程唤醒。当其他线程调用cond.signalAll()时,所有await在这个cond上的线程被唤醒,然后进行条件判断:cond_is_achieve,如果不满足,将继续进入await,否则,跳出这个循环,执行其他逻辑。

除了signalAll之外,还有signal方法,它随机选择一个await的线程解锁,但是这样的话,非常容易导致死锁,因此不建议使用。

21、synchronized关键字的作用和ReentrantLock基本是一致的:

public synchronized void method()
{
    // all be locked, one thread at any moment !!!
}

22、如上所示,当使用synchronized时,实际上内部有一个锁对象,可以直接调用wait()和notifyAll()实现条件等待。(实际是Object实现了这个,注意Lock和Condiction是await()和signalAll()有所区别)

23、如果将静态方法声明为synchronized,则将锁住整个.class,其他线程此时将不能再调用该类的其他静态方法。

24、使用synchronized处理同步问题更为简单,因此应该优先使用,不能满足时再用Lock/Condiction。

25、还有另外一种不对整个函数synchronized,而只同步部分的方法:自己建一个Object对象,如下:

class ClassXX
{
    public void func()
    {
        synchronized (lock)
        {
            // partly sync
        }
    }
    Object lock = new Object();
}

26、锁和条件是有用的条件,但它们不是OO的,一种较新的技术是“监视器”,它具有如下特性:

(1)监视器是只包含私有域的类
(2) 每个监视器只有一个相关的锁,可以有若干条件
(3)使用该锁对所有

27、如果仅仅是为了同步一两个变量,可以用volatile。一个域加上volatile后,编译器就知道它可能被其他线程共享,从而不会进行可能导致危险的编译优化。

28、但是volatile本身不提供原子操作,因此,像+=这种还是危险的,而简单的直接get、直接set是可以的。因此,建议只让volatile修饰boolean。。。

29、还有AtomicBoolean、AtomicInteger等,提供了同步、原子操作如getAndAdd+=,可以使用。

30、怎么判断一个域的存取是安全的?只要满足下述三个条件之一:
(1)域是final的,并且在构造调用完成之后被访问
(2)对域的访问有公有的锁保护
(3)域是volatile,且没有其他危险原子操作

31、银行转帐问题是有可能会发生死锁的,此时按Ctrl + \可以看到死锁线程条件。Java没有提供条件打破死锁,因此必须好好设计,以避免死锁。

32、直接的lock是锁定,可能会导致死锁,可以用trylock,如果申请失败,就去做其他的事情。

33、trylock、await也有带时间参数的版本,当时间到了还没成功,就取消阻塞返回。

34、Java还提供了读写锁:java.util.concurrent.locks.ReentrantReadWriteLock。写操作与读操作互斥。简言之,多读一写。
读锁:可以被多个读操作共享的读锁,但会排斥所有的写操作。
写锁:得到一个写锁,排斥所有其他读操作和写操作。

(1)创建读写锁:

ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

(2)抽出读锁和写锁:

ReentrantReadWriteLock.ReadLock rlock =  rwlock.readLock();
ReentrantReadWriteLock.WriteLock wlock = rwlock.writeLock()

(3)对所有读加读锁,对所有写加写锁:

rlock.lock();

//......

rlock.unlock();

wlock.lock();

//......

wlock.unlock()l

35、JDK从1.2开始就已经不再推荐使用suspend和stop,应该避免使用它们。

36、上面的多线程同步都是Java提供的最基础方法,更多的时候,我们可以选择已经设计好的模块。

37、很多多线程设计模型都可以转化成队列:生产者线程把任务插入队列,消费者线程取出它们后再处理。Java提供了阻塞队列,当队列满插入时,或者队列为空取出时,都将阻塞。

BlockingQueue(接口):
异常/只返回false或者null
add/offer 添加
element/peek 返回头,不删除
remove/poll 删除头返回
以下两个方法是阻塞的:
put/take 放、取
此外,offer和poll都有带超时的版本。

38、上面的BlockingQueue只是个接口,Java提供了几个变种实现:
(1)LinkedBlockingQueue:可无限长度(也可限定) ,双端
(2)ArrayBlockingQueue:需要指定容量 ,可有公平策略(等待最长的获得get)
(3)PriorityBlockingQueue: 无线长度,带优先级,最优先的先被移出,如果队列空,阻塞。
(4)DelayQueue:可以延迟取回。

39、一个阻塞队列的例子:一个线程产生随机数,其他N个线程把它打印出来,有点小无聊,主要是使用阻塞队列实现这种同步模型任务。

import java.util.concurrent.*;
import java.util.*;

class GenRunnable implements Runnable
{
	public GenRunnable(BlockingQueue<Integer> bq)
	{
		this.bq = bq;
	}

	public void run()
	{
		Random rand = new Random();
		while(true)
		{
			try
			{
				bq.put(rand.nextInt());
				Thread.sleep(10);
			}
			catch(InterruptedException exp)
			{
				// Just break the loop
				break;
			}
		}
	}

	private BlockingQueue<Integer> bq;
}

class PrintRunnable implements Runnable
{
	public PrintRunnable(BlockingQueue<Integer> bq)
	{
		this.bq = bq;
	}

	public void run()
	{
		while(true)
		{
			try
			{
				System.out.println(bq.take());
				Thread.sleep(200);
			}
			catch(InterruptedException exp)
			{
				//Break the loop
				break;
			}
		}
	}

	private BlockingQueue<Integer> bq;
}

public class BQTest
{
	public static void main(String [] args)
	{
		BlockingQueue<Integer> bq = new LinkedBlockingDeque<Integer>();
		Thread tw = new Thread(new GenRunnable(bq));
		tw.start();
		for(int i=0; i< 100; i++)
		{
			Thread tr = new Thread(new PrintRunnable(bq));
			tr.start();
		}
	}
}

40、除了上面的阻塞队列外,Java也提供了Map、Set的线程安全版本,常用的有:
(1)ConcurrentHashMap
(2)ConcurrentLinkedDeque
(3)ConcurrentLinkedQueue
(4)ConcurrentSkipListMap
(5)ConcurrentSkipListSet

它们的size()操作耗时较长(需要遍历所有的)、支持大量读和少量的写者,默认可以有16个同时写(通过concurrencyLevel参数控制)。超过这个值的写线程将被阻塞。
其他操作,如replace替换、putIfAbsent如果没有则插入等都在内部进行了线程同步处理,是原子操作。

41、CopyOnWriteArrayList和CopyOnWriteArraySet也是线程安全的,所有修改都对底层数组进行了复制。好处是当迭代的时候,返回前一次的修改,迭代是完全安全的(但可能不新)。

42、JDK1的Vector和Hashtable都是线程安全的,JDK1.2之后就弃用了,而ArrayList等都不是线程安全的。可以用同步包装器来搞定:Collections.synchronizedListList/Map等。但是,使用迭带器的时候(或者遍历的时候),仍然需要自己进行线程同步(锁来保护部分代码或者synchHashMap)

43、一般情况下,ConcurrencyXXX的性能好于包装的synchronizedXXX,或者CopyOnWriteXXX,除了包装的ArrayList,它比CopyOnWrite好。

44、除了Runnable之外,还有一个Callable,它可以看作特殊的Runnable:有返回值。

Interface Callable<V>
{
    V	call();
}

只有这一个函数,返回值类型是泛型V,例如Callable<Integer>,则表示Callable的返回类型是Integer。

45、上面也看到了,Callable只有一个函数,我们只能直接调用(call)它,怎么和多线程结合起来呢?答案是Future接口(一般是用FutureTask这个子类):

Callable<Integer> callint = ....;
FutureTask<Integer> task = new FutureTask<Integer>(callint);
Thread t  = new Thread(task);
t.start();
...
Integer result = task.get();

最后这个get有阻塞版本和带时间的版本,这个默认的是阻塞版本,只有Callable运行完毕后,才会返回。否则将阻塞。

可以用cancel取消计算。下面的例子用单独的线程异步计算,并在主线程中get它:

import java.util.concurrent.*;

public class FutureTest
{
    public static void main(String [] args)
    {
        FutureTask<Integer> task = new FutureTask(new Callable<Integer>()
                {
                    public Integer call()
                    {
                        int sum = 0;
                        for(int i=1; i<=100; i++)
                        {
                            sum+=i;
                        }
                        return sum;
                    }
                });
        Thread t = new Thread(task);
        t.start();
        try
        {
            System.out.println(task.get());
        }
        catch(InterruptedException ie)
        {   

        }
        catch(ExecutionException ee)
        {
        }   

    }
}

46、如果程序中线程的生命周期很短,应该考虑现线程池。另一个好处是可以限制并发线程数量(放置突发流量挂掉进程)。

47、Java使用执行器(Executor) java.util.concurrent.Executors用来创建线程池:
(1)newCachedThreadPool:必要时才创建新线程,空闲线程保留60秒。
(2)newFixedThreadPool:包含固定数量的线程,空闲线程一直保留。
(3)newSingleThreadExecutor:只有一个线程的“线程池”,实际是顺序提交。
(4)newScheduledThreadPool:用于定时执行任务,替代java.util.Timer
(5)newSingleThreadScheduledExecutor:用于预定执行的线程池。

48、Java的线程池用ExecutorService表示,提供的下列方法可用于将Runnable、Callable等逻辑直接转化为线程的“任务”:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

在调用上述submit时,会得到一个Future对象,用于将来查询结果。最后一个版本由于Runnable无返回,因此这个Callable只能isDone,等,get返回的是个null。其他的可以获取返回值。

49、当用完线程池的时候,应该shutdown。此时线程池不再接受新任务,但需要到所有之中的任务都执行完,才能完全释放。

50、使用线程池实现多线程的流程:
(1)Executors.newFixedThreadPool,或者其他线程池
(2)submit一个Runnable或者Callable,保存好Future
(3)如果想取消, 直接在上一步的Future上cancel。
(4)当不再需要提交任何任务时,提交shutdown。

51、ScheduledExecutorService用于预定执行重复的任务。schedule()方法或者schedulerWithFiexedRate()或者scheduleWithFixedDelay,比较实用。

52、如果想立即结束线程池中所有任务,可以实用shutdownNow。

53、invokeAny提交所有对象到一个Callable对象集合中,只返回最先完成的任务,对于RSA密码分解这种,你提交多种分解拆解,最后能成功的那个会返回,就可以模拟并行计算了。实际是获得解空间的一个可行解。

54、invokeAll提交任务到所有Callable集合中,实际是获得解空间的所有解。

55、Java还提供了一些让各个线程“相互合作”的对象。
(1)CyclicBarrier:创建一个“栅栏 ”,llows a set of threads to all wait for each other to reach a common barrier point。相当于“预备。。。条件发生时再一起跑”,比较贴切了吧。
(2)CountDownLatch:A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. 等待,直到指定数目个线程事件完成后。(可以用于等待,直到所有线程都执行完毕)
(3)Exchanger:
(4)Semaphore:信号量,限制线程访问资源的数量,很传统不解释了。
(5)SynchronousQueue:允许一个线程把对象交给另一个线程?

56、信号量: Semaphore.acquire()/Semaphore.release(),也可以try

57、构造一个n个线程参与的CyclicBarrier:

CyclicBarrier barier = new CyclicBarrier(nThds);

每个线程完成一些工作后(不一定是执行完run),调用:

barrier.await();

提供给栅栏一个可选的动作,当栅栏够了后,去执行这个action。

new CyclicBarrier(thds, barrierAction);

这个栅栏是循环的,不同于CountDown,它有reset,可以重置。

58、交换器Exchanger,典型的是:一个线程向缓冲区填充数据,另外一个线程消耗这些数据,但他们完成后,交换缓冲区。

59、同步队列:一个线程调用SynchronousQueue的put方法时,它会阻塞直到另外一个线程take为止反之毅然,这个很有意思。。。

swing的不研究了,本章结束,卷I结束:-)

 

Leave a Reply

Your email address will not be published. Required fields are marked *