第十六章 多线程(一)
并行:在同一时刻,有多条指令在多个处理器上同时运行
并发:同一时刻只有一个指令被处理器运行,但多个进程指令快速轮换执行,使得宏观好像多个指令在同时运行
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程
线程的创建和启动
1.继承Thread类创建线程
步骤:1.定义Thread子类,重写run(),run()即为线程执行体
2.创建Thread子类实例,即创建线程对象
3.调用线程对象的start()方法启动线程
线程之间无法共享线程类的实例变量
2.实现Runnable借口创建线程类
步骤:1.定义Runnable接口,重写run()方法
2.创建Runnable实例,以此实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象
可用Lambda表达式创建实例
3.调用线程对象的start()方法来启动线程
eg: new Thread(RunnableThread, RunnableName).start()
多线程共享实现线程类的实例变量
3.使用Callable和Future创建线程
步骤:1.创建Callable实现类,并实现call()方法
call()是只执行体,且有返回值
可使用Lambda表达式创建Callable对象
2.使用FutureTask类包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
3.使用FutureTask对象作为Thread对象的target创建并启动新线程
4.使用FutureTask对象的get()方法来获得子线程执行结束后返回值
eg:
FutureTask<Integer> task = new FutureTask<Integer>(CallableThread);
new Thread(task, taskName).start()
call()方法可以有返回值
call()方法可以抛出异常
Future接口定义如下方法;
boolbean cancel(boolbean mayInterrupIfRunning) 取消Future关联的callable任务
get() 获取线程返回值,或阻塞。直到拿到返回值
isCancelled() 如果任务正常完成前被取消,返回true
isDone() 任务完成返回true
4.创建线程的三种方式的对比
使用Runnable、Callable接口的优缺点:
优点:1.线程类只是实现Runnable接口和callable接口,还可以继承其他类
2.多个线程可以共享一个target对象,非常适合多个相同线程处理同一份资源的情况下。从而可以将CPU、代码、和数据分开。
劣势:编程复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法
使用Thread类创建线程优缺点:
优点:编写简单,如果需要访问当前线程直接使用This
缺点:已经继承Thread,无法再继承其他父类
线程的生命周期:
新建new - 就绪Runnable - 运行Running - 阻塞Blocked - 死亡Dead
1.new一个线程之后,是新建状态
2.start()执行后,就绪状态
3.如果就绪状态的线程获得CPU,开始执行run(),则是出于运行状态
4.如下情况会进入阻塞状态:
1.线程调用sleep()
2.调用了阻塞式IO
3.线程试图获得一个同步监视器,但是该同步监视器正在被其他线程占用
4.线程在等待某个通知notify
5.程序调用了线程的suspend()方法将该线程挂起 / 可通过resume()恢复
被阻塞的线程阻塞结束后,会再次进入就绪状态,再次等待CPU,然后进入运行状态
yield()方法可使运行状态转为就绪状态
5.线程会以一下三种方式结束,结束后处于死亡状态
1.run()、call()方法执行完毕,线程正常结束
2.线程抛出未捕获异常和错误
3.直接调用stop()方法结束线程
可通过isAlive()方法,判断线程是否死亡
Note:程序只能对新建状态的线程执行start()方法。只能执行一次。
控制线程:
join线程:当程序调用其他线程的join()时,调用线程将会被阻塞,直到join()方法加入的join线程执行完毕,调用线程才会继续执行。
后台线程:在后台运行,为其他线程提供服务。如果所有线程都死亡,后台线程会自动死亡。
Thread对象的setDaemon(true)方法可将制定线程设置为后台线程
isDaemon()方法用于判断线程是否为后台线程。
前台线程创建的线程默认为前台线程,后台线程创建的线程默认为后台线程。
要将某个线程设置为后台线程,必须在该线程启动前设置,setDaemon(true)必须在start()方法前调用
线程睡眠:sleep(),让目前正在执行的线程暂停一段时间,线程进入阻塞状态,时间到了之后,进入就绪状态
sleep()和yield()的区别:1.sleep()方法执行后,线程阻塞,然后让其他线程执行;yield()方法只会给优先级相同,或者优先级更高的线程执行机会。
2.sleep(),让目前正在执行的线程暂停一段时间,线程进入阻塞状态,时间到了之后,进入就绪状态;yield()方法不会将线程进入阻塞状态,直接进入就绪状态。
3.sleep()方法声明抛出InterruptedException异常,所以sleep需要捕捉该异常,或者显示声明抛出异常;而yield()没有声明抛出任何异常
4.sleep()方法比yield()方法更具有移植性
设置线程优先级:
每个线程优先级默认与父线程优先级相同。
main线程具有普遍优先级,main创建的子线程具有普遍优先级。
setPriority()设置并返回线程优先级,参数整型,1~10; MAX_PRIORITY 10;MIN_PRIORITY 1; NORM_PRIORITY 5
线程同步:
同步代码块:
synchronized(obj)
{
同步代码块
}
程序运行行,必须先获得对同步监视器的锁定;任何时刻只有一个线程可以获得对同步监视器的锁定。
在加锁期间其他线程无法修改该资源。
同步方法:
public/private/protect synchronized void mothodname()
{
方法体
}
synchronized不可以修饰static方法
无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
synchronized可以修饰代码块和方法但是不可以修饰构造器,成员变量。
同步锁:显示定义同步锁对象实现同步。
1.定义锁对象:private final ReentrantLook lock = new ReentrantLock();
2.加锁:lock.lock()
3.执行方法体
4.释放锁:lock.unlock()
Lock控制多个线程对共享资源进行访问。通常所提供独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前需要先获得Lock对象。
ThreadLocal线程局部变量
ThreadLocal:线程局部变量,为每一个使用该变量的线程都提供一个变量值的副本,使得每一个线程都可以独立的改变自己的副本,而不会与其他线程的副本产生冲突。
T get(); 返回此局部变量在该线程中的副本值
void remove():删除此局部变量在该线程中的值
void set(T value):设置此局部变量在该线程中的值
eg: private ThreadLocal<String> name = new ThreadLocal();
name.get();
name.set("nameA");
name.remove();
死锁
当两个线程互相等待释放同步监视器时就会发生死锁。
线程通信
1.synchronized关键字
对于使用synchronized修饰的同步方法,该类默认实例(this)是同步监视器,可以直接调用这三个方法。
对于使用synchronized修饰的同步代码块,synchronized后的对象是同步监视器,该对象调用这三个方法。
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或者notifyAll()方法唤醒该线程
notify():唤醒此同步监视器上等待的单个线程
notifyAll():唤醒此同步监视器上等待的所有线程
2.lock()
await():类似wait()。导致当前线程等待,直到其他线程调用该Condition的signal()或者signalAll()方法唤醒该线程
signal():唤醒此Lock对象上等待的单个线程
signalAll():唤醒此Lock对象上等待的所有线程
使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue继承自Queue接口
当生产者线程试图向BlockingQueue放入元素,如果队列已经满了,则该线程被阻塞;当消费者线程试图从BlockingQueue取出元素,如果队列是空的,则该线程被阻塞。
put(E e):尝试把E元素放入BlockingQueue,如果队列已经满了,则该线程被阻塞
take():从BlockingQueue取出元素,如果队列是空的,则该线程被阻塞
有以下五个实现类:
ArrayBlockingQueue:基于数组实现BlockingQueue
LinkedBlockingQueue:基于链表实现BlockingQueue
PriorityBlockingQueue:
SynchronousQueue:同步队列。对该队列的存取操作必须交替进行
DelayQueue:
线程组
ThreadGroup表示线程组,可以对一批线程分类处理
用户创建的线程都属于指定线程组,如果没有显示确定是哪个线程组,则属于默认线程组。默认情况下,子线程与父线程属于同一线程组。
一旦线程加入指定线程组,线程运行中不可以改变线程组,直到死亡
显式指定线程组:
Thread(ThreadGroup group,Runnable target)
Thread(ThreadGroup group,Runnable target,String name) //指定线程名字
Thread(ThreadGroup group,String name)
ThreadGroup创建实例
ThreadGroup(String name)
ThreadGroup(ThreadGroup parent, String name)
int activeCount() //返回此线程中活动线程的数目
interrupt() //中断该线程组所有线程
isDaemon() //判断该线程是否是后台线程组
setDaemon(boolean daemon) //把该线程设置为后台线程组
setMaxPriority(int pri) //设置线程组的最高优先级
线程池
返回ExecutorService对象,该对象代表一个线程池:
newCachedThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool(int nThreads) 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newSingleThreadExecutor() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
返回ScheduledExecutorService对象,该对象代表一个线程池:
newScheduledThreadPool(int corePoolSize) 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadScheduledExecutor 创建只有一个线程的线程池,可以在指定延时后执行线程任务
ExecutorService newWorkStealingPool(int parallelism) 创建持有足够线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争
ExecutorService newWorkStealingPool(int parallelism) 前一个简化版本,例如4个cpu,则并行级别设置为4
ExecutorService代表尽快执行的线程池,只要有空闲线程就执行任务。
Future<?> submit(Runnable task):将一个Runnable对象提交给指定线程池,在有空闲线程时执行。因为run()方法没有返回值,所有该方法返回null
<T>Future<T> submit(Runnable task, T result):将一个Runnable对象提交给指定线程池,在有空闲线程时执行。指定返回值,在执行结束后返回result
<T>Future<T> submit(Callable<T> task):将一个Callable对象提交给指定线程池,在有空闲线程时执行,返回Future
ScheduledExecutorService代表可在指定延迟后或者周期性执行线程任务的线程池。
ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):指定Callable任务在delay延时后执行
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):指定command任务在delay延时后执行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):指定command任务在delay延时后执行,以设定频率重复执行
执行:initialDelay initialDelay+peroid initialDelay+2 * peroid ....
ScheduledFuture<?> scheduleAtFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后一次执行结束下次开始前都存在给定延时
包装线程不安全的集合:
使用Collections提供的类方法可以把线程不安全的集合包装为线程安全
<T> Collection<T> synchronizedCollections(Collections<T> c): 返回指定集合对应的线程安全的集合
static <T> List<T> synchronizedList(List<T> list): 返回指定list对象对应的线程安全的list对象
static <K,V> Map<K,V> synchronizedMap(Map<K,V> map): 返回指定Map对象对应的线程安全的Map对象
static <T> Set<T> synchronizedSet(Set<T> set): 返回指定Set对象对应的线程安全的Set对象
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m): 返回指定SortedMap对象对应的线程安全的SortedMap对象
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s): 返回指定SortedSet对象对应的线程安全的SortedSet对象
eg:HashMap m = Collections.synchronizedMap(new HashMap()); //把一个普通的HashMap包装为线程安全的HashMap对象
线程安全的集合类:
分为两类:
Concurrent开头的:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque
CopyOnWrite开头的:CopyOnWriteArrayList、CopyOnWriteArraySet
Concurrent开头的表示支持并发访问的集合,可以支持多线程并发写入访问,并且都是安全的,但读取操作不必锁定;CopyOnWrite开头的底层通过复制实现,当对它操作时,实际是对数组副本进行操作,因此线程安全。CopyOnWrite由于写入需要频繁复制数组,性能比较差,但是在读操作时,不需加锁,操作很快很安全,适合读取操作远远多于写入操作的情况,比如缓存。