多线程
1.线程概述
线程是进程的执行单元,进程具有:独立性,动态性,并发性三个特征。线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程,线程可以有自己的堆栈,自己的计数器,自己的局部变量,但不拥有系统资源,他与父进程的其他线程共享该进程所拥有的全部资源。线程之间的运行是独立性的,但其执行是抢夺式的,但一个线程可以操作其他的线程,同一进程的线程之间可以并发执行。
简而言之,一个程序运行之后至少有一个进程,一个进程可以包含多个线程,但至少包含一个线程。(操作系统可以同时执行多个任务,每个任务就是进程;进程可以执行多个任务,每个任务就是线程)
并发:指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行。
并行:同一时刻,有多条指令在多个处理器上同时执行。
2.线程的创建与启动
2-1,继承Thread类
步骤:
1.定义Thread的子类,重写其run()方法。
2.实例化该类。
3.调用线程对象的start()方法来启动该线程。
2-1,实现Runnable接口
步骤:
1.定义Runnable接口的实现类,实现其run()方法。
2.创建Runnable实现类的实例,并以此实例作为Thread的参数来创建Thread对象。
3.调用线程对象的start()方法来启动该线程。
2-3,使用Callable和Future创建线程
步骤:
1.创建Callable接口的实现类,并实现call()方法。(call()方法将作为线程的执行体,有返回值)创建Callable的实例。
2.使用FutureTask类来包装Callable对象。(该FutureTask对象封装了Callable对象的call()方法的返回值)
3.使用FutureTask对象作为Thread对象的参数创建并启动线程。
4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
className rt = new className();
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{//线程运行的内容});
new Thread(task).start();
task.get();
注意:Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法的返回值类型相同。
而且Callable接口是函数式接口(类似于ES6中声明函数)
2-4.三种方式的比较
第一中方法,不能再继承其他类,有局限性;第二种方法还可以实现继承其他类,推荐使用;第三种方法也可以继承和实现其它类,但过程有些复杂。
3.线程的控制
3-1.后台线程
后台线程可以说是为前台线程服务的线程,比如垃圾回收机制。后台线程会在前台线程死亡后自动死亡,当人为强制结束一前台线程时,无论后台线程是否执行完,都会死亡。我们一般自己定义的线程都是前台线程,当然也可以通过Thread的setDaemon(true);方法设置一线程为后台线程,次设置要放在start();方法前。判断一个线程是否为后台线程可以用Thread类的isDaemon();方法。
3-2.线程控制
首先,线程有新建,就绪,运行,死亡,阻塞这五种状态。
join()方法:调用此方法的线程对象会一直占用cpu的执行权,知道该线程执行结束,才允许当前其它线程抢夺cpu执行权。
sleep()方法:让线程睡眠一定的时间,不会释放资源,相当于使该线程进入阻塞状态,没有释放锁。
yield()方法:让该线程暂停执行,并进入就绪状态,不会阻塞该线程,不释放锁。
wait()方法:让该线程进入等待状态,在被唤醒之前都不会执行,会释放cpu资源,也释放了锁,也有和sleep一样的时间参数。
notify()和notifyAll()方法:用来唤醒线程,与wait()方法结合使用。
等待唤醒必须写在同步代码块中,也就是说等待和唤醒的都是同步的线程。
4.线程的同步
线程的同步目的是为了解决多个线程对只有一份的资源共同访问带来的资源安全问题,这一问题的原因是线程的执行是抢夺式的。解决这一问题可以通过同步代码块,同步方法和同步锁来解决。
4-1.同步代码块
只需要在需要的步的代码块外用synchronized(){}包起来,并给其传人加锁的对象,便可以使得同步代码块中的代码在执行时一直占用cpu执行权,在代码执行完后会释放锁。这样别的线程在要操作加锁的对象时,会失败,因为同一时刻只能有一个线程拿到锁。
4-2.同步方法
同步方法是在一个方法前加上synchronized关键字,使其变为同步方法。其锁是调用此方法的对象,也就是this。
要注意的是synchronized只能修饰代码块和方法,不能修饰其他。
锁会在同步代码执行完后,遇到wait()方法或者发生错误后释放,在其他情况下都不会释放锁。
4-3.同步锁
同步锁是Java 5后通过显示的定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
private final ReentrantLock lock = new ReentrantLock( );
ReentrantLock(可重入锁)是Lock接口的实现类,Lock,ReadWriteLock是Java 5提供两个根接口,ReentrantReadWriteLock是ReadWriteLock接口的实现类,ReentrantReadWriteLock类为读写操作提供了三种锁模式:Writing,ReadingOptimistic,Reading。Java 8还增加了StampedLock类。
在使用是通过lock.lock()和lock.unlock()将同步的代码包起来就可以了。
5.线程的通信
5-1.等待唤醒机制
wait()和notify()这两方法在Object中,没在Thread中,同时等待唤醒必须写在同步代码块中,也就是说等待和唤醒的都是同步的线程。
5-2.使用Condition控制线程通信
如果线程中没有synchronized关键字来保证同步,而是使用Lock来同步,就可以使用Condition类,他可以让得到Lock对象却无法继续执行的线程释放Lock对象。
private final ReentrantLock lock = new ReentrantLock( );
private final Condition cond = lock.newCondition( );
这里主要有三个方法和wait(),notify(),notifyAll()相对应,await(),signal(),signalAll()。因为Condition将前三个方法分解成了其他不同的对象。
5-3.使用阻塞队列控制线程通信(不好用)
Java提供了一个BlockingQueue接口,他是Queue的子接口,主要用途是作为线程同步的工具,而不是容器。他有一个特征,当线程要向队列放入元素时,如果队满,则线程被阻塞;当线程从队列中取元素时,如果队列以空,则线程进入阻塞。这样就可以控制线程的阻塞。
BlockingQueue提供了两个支持阻塞的方法:put(E e)和take(E e)。
6.线程池
当系统启动一个新线程的成本是比较高的(创建新的PCB等),在使用线程池后可以很好的提高性能。
线程池在系统启动时会创建大量空闲的线程,当一个Runnable或Callable对象传给线程池时,线程池会启动一个线程来执行他们的run()或call()方法,当run()或call()方法执行完后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待下一次启动。
使用线程池也可以有效的控制系统中并发线程的数量。具体的使用请看我的另一篇文章:Java线程池(未写)。