说来惭愧,距离第一次发布Java的博客已经过去整整一个多月了,可恶的是我居然刚到多线程这里徘徊,这效率可真是急煞人也,兴许是中间参杂了一些其他的技术课程给耽搁了,感觉每天都很忙,备忘录里一大堆的事情数都数不过来,想好好总结一下刚结束的网络程序设计C#+SQLSever建站过程,又想将`scrapy框架添个redis再梳理梳理,还忙着复习复习下周就结课的操作系统,又要抽空看看多媒体的PCA,特征脸,K-L变换,最近又接了个牛客网大使的活,马上双十一也来了,又想去蹲点捡个垃圾建个NAS玩玩。。等等备忘录事件,所以 :) 很忙。忙归忙,活还是要干的,今天来梳理梳理Java的多线程知识
想要理解多线程先来了解多进程,线程和进程有很大的关系
多进程
进程是程序在操作系统上运行的过程,是系统进行资源分配和调度的独立单位,一个程序可由多个进程共用,一个进程活动时可顺序执行若干个程序,进程包括程序、数据和PCB(进程控制块) ,(并发是指一段时间内同时运行多个进程,不是同步;并行是指在一个时间点同时运行多个进程,同步)
多进程实际上是CPU交替轮流执行多个进程的结果,是并发的
多线程
在一个进程内部可执行多个任务,一般将进程内部的任务称为线程。
线程是在进程的概念基础上提出来的, 线程和进程都是与计算机中的并发执行相关概念
在一个进程内的多个线程是共享一个存储空间的,在多进程程序中通信切换进程时需要改变地址空间位置,而在多线程程序只需要改变执行次序,因为它们都位于同一个存储空间内。
Java多线程
在Java中只有实现类Runnable接口的类对象才能称为线程。Java提供了两种途径实现多线程:
一、继承Thread类(该类已实现Runnable接口),
二、直接实现Runnable接口。
一、Thread类
Thread类属于java.lang包,故系统运行时会自动导入,Thread已经实现Runnable接口,故只需要让一个类继承Thread类,并将线程的代码写在run()方法中,即重写Thread类的run()方法即可创建线程。
Thread多线程类定义如下
[public] class 类名 extends Thread{
属性;
方法;
public void run(){
//线程程序代码
}
}
一看就懂,接下来实战一下
EG:
public class ThreadDemo1 extends Thread{
private String name;
public ThreadDemo1(String name){
setName(name);
}
public void run(){
for (int i=0;i<10;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String []args){
ThreadDemo1 t1=new ThreadDemo1("xiancheng1");
ThreadDemo1 t2=new ThreadDemo1("xiancheng2");
t1.start();
t2.start();
}
}
上面的代码中值得注意的是start()方法和set Name以及getName方法,
因为线程的运行需要本机操作系统的支持,所以通过start()方法启动线程。而且一个线程对象只能调用一次start方法,否则会抛出“IllegalThreadStateException”异常,set Name和getName方法是Thread类的静态方法,用来设置和得到线程名称。
二、Runnable接口创建线程
通过实现Runnable接口的抽象方法run()方法即可
定义
[public] class 类名 implements Runnable{
属性;
方法;
public void run(){
//线程程序代码
}
}
EG:
import java.util.Date;
class ThreadDemo implements Runnable{
public void run(){
for(int i=1;i<=5;i++){
System.out.println("now "+Thread.currentThread().getName()+", "+i+" "+(new Date()));
}
}
}
public class ThreadDemo2 {
public static void main(String []args){
ThreadDemo tm=new ThreadDemo();
Thread t1=new Thread(tm,"线程1");
Thread t2=new Thread(tm,"线程2");
t1.start();
t2.start();
}
}
上面因为Runnable接口对线程没有任何支持,因此在获得线程实例后,必须通过Thread类的构造方法来实现。
说到这里就不得不说一说Runnable和Thread到底用哪个比较合适了,
比如:在一个售票系统中:
使用继承Thread类来设计
class ticket extends Thread{
private int tick=5;
private String name;
public ticket(String name){
setName(name);
}
public void run(){
while(tick>0)
if(tick>0){
System.out.println(Thread.currentThread().getName()+"卖出"+(tick--)+"张票");
}
}
}
public class ThreadDmo4 {
public static void main(String []args){
ticket window1=new ticket("first");
ticket window2=new ticket("second");
window1.start();
window2.start();
}
}
可以看出上面Thread实现的窗口售票显然是不合适的
用Runnable接口实现
class ticket1 implements Runnable{
private int tick=5;
private String name;
public void run(){
while(tick>0)
if(tick>0){
System.out.println(Thread.currentThread().getName()+"卖出"+(tick--)+"张票");
}
}
}
public class ThreadDemo5 {
public static void main(String []args){
ticket1 window=new ticket1();
Thread t1=new Thread(window,"first");
Thread t2=new Thread(window,"second");
t1.start();
t2.start();
}
}
上面虽然也是两个线程,但是每个线程都是调用同一个run()方法,访问的都是同样的资源,所以实现了共享。
其实,上述问题也可以用static变量来实现,只是容易发生资源被抢占的风险(即另一个窗口迟迟无法售票)。
线程的状态(参考Java并发编程:线程的基本状态)
一、线程的基本状态
线程基本上有5种状态,分别是:NEW、Runnable、Running、Blocked、Dead。
1)新建状态(New)
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2)就绪状态(Runnable)
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3)运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中
4)阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1、等待阻塞
运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2、同步阻塞
线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3、其他阻塞
通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注意:对于Java的状态是一个动态的概念,Java的application应用程序的主线程是main()方法的执行步骤,对于Java的applet小应用程序,其主线程是按其声明周期执行的步骤。
线程的部分基本方法
获取并设置线程的名称
Thread.currentThread() //可获得当前线程的对象引用
一般若线程没有设置线程名称,系统会自动命名,当然也可以使用setName去设置,使用getName去获得
线程的优先级
在Java中每个线程都有优先级,一般取值范围是:1~10,默认为5,可以使用Thread类中的setPriority()方法设置一个线程的优先级,当然啦范围必须在1~10内,否则会产生异常。在Java中定义了三种优先级,MIN_PRIORITY
(最低表示为1),MAX_PRIORITY
(最高表示为10),NORM_PRIORITY
(默认表示为5)。
示例:
class ThreadDemosix implements Runnable{
public void run(){
for(int i=1;i<=10;i++){
System.out.println("now:"+Thread.currentThread().getName()+", i="+i);
}
}
}
public class ThreaDemo6 {
public static void main(String []args){
ThreadDemosix tm=new ThreadDemosix();
Thread t1=new Thread(tm,"name-1");
Thread t2=new Thread(tm,"name-2");
Thread t3=new Thread(tm,"name-3");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(2);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
事实上从程序的运行结果来看,线程的优先级似乎并没有起到什么太大的作用。
故注意:优先级高的线程只是优先级获得CPU,不是绝对获得CPU。此外主线程的优先级为默认值5。
线程的休眠
说白了就是指线程暂时处于阻塞状态,使用sleep,join,yield方法可以使得线程阻塞,然而它们又有着很大的不同。
sleep
1、 sleep是静态方法,在主方法内无论通过线程对象去调用sleep还是直接通过Thread.sleep形式去调用sleep方法,其都是休眠主线程;如果想要线程实例休眠,sleep方法应该放入run()方法里。
2、 当线程处于sleep时,如果线程被中断,则会抛出Interrupted Exception异常,中断线程可以使用Thread类提供的interrupt方法。
join
假设当前运行的线程为A中调用了线程B的join()方法,则A会等待B的执行,
1、如果join中没有指定时间,则线程A会等待B运行结束后才由阻塞转变为就绪状态,然后等待获取CPU。
2、如果join中指定了时间,且线程B还没有运行完,则线程A也会在时间结束时,从阻塞状态转变为就绪状态。
3、如果join指定了时间,且线程B已经执行完了,则线程A立马从阻塞状态转变为就绪状态。
yield
yield方法指当前正在运行的线程退出运行状态,暂时让给其他线程先执行,可通过Thread.yield()方法实现,该方法只能把运行权让出来,让出后,哪个抢到就是哪个的,且抢到的优先级只能大于等于当前的。而sleep则不管。
sleep是让当前线程转到阻塞状态,而调用yield则将当前线程转到就绪状态
sleep会抛出异常,而yield不会抛出任何异常
sleep具有更好的移植性。
线程同步
线程安全指多线程访问同一代码,不会产生不确定的结果
回到那个售票系统
得到结果
只是添加一个0.1秒的休眠竟然会出现这种情况,当然是不能够容忍的,假若这个休眠模拟的是网络延迟,那这个售票系统无疑得跪,故而寻找解决办法。
在Java中解决同步问题的方法有三种,其一为同步代码块,其二为同步方法,其三是JDK1.5之后加入的同步锁(需要引入包java.util.concurrent.locks.ReentrantLock)
同步代码块
synchronized(Object obj){
//同步的代码
}
package test;
class ticket1 implements Runnable{
private int tick=5;
private String name;
public void run(){
while(tick>0) {
synchronized (this) { //添加同步代码块
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");
}
}
}
}
}
public class ThreadDemo5 {
public static void main(String []args){
ticket1 window=new ticket1();
Thread t1=new Thread(window,"first");
Thread t2=new Thread(window,"second");
t1.start();
t2.start();
}
}
只需添加进同步代码块,问题即解决,十分方便
同步方法
[访问控制符] synchronized 返回类型 方法名(参数列表){
//需要同步的代码
[return 返回值]
}
package test;
class ticket1 implements Runnable{
private int tick=5;
private String name;
public void run(){
while(tick>0){
this.sa(); //调用同步方法
}
}
public synchronized void sa(){ //同步方法
while(tick>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");
}
}
}
}
public class ThreadDemo5 {
public static void main(String []args){
ticket1 window=new ticket1();
Thread t1=new Thread(window,"first");
Thread t2=new Thread(window,"second");
t1.start();
t2.start();
}
}
资源问题也得到了解决
同步锁
在Java中,任何一个对象都有一个同步锁(设O为对象)
- 对象O的同步锁在任何时刻最多只能被一个线程拥有
若对象O的同步锁被线程T 拥有,则当其他线程访问O时,线程将被放到O的锁池中,并将它们转化为同步阻塞状态。
拥有O的锁的线程T执行完后,会自动释放O的锁,若执行中,线程T发生异常退出,则也将自动释放O的锁
若线程T在执行同步代码块时,调用了O的wait()方法,则线程T同样会是否O的锁,线程T也将进入阻塞状态
如果线程T在执行同步代码块时,调用了Thread类的sleep方法,线程T将放弃运行权,即放弃CPU,但线程T不会放弃对象O的锁,即其他线程无法执行此同步代码块
- 如果线程T释放了对象O的锁,并放弃了运行权,则CPU将会随机分配给对象O锁池中的线程,该线程也将获得对象O的锁。
以上述售票系统为例
import java.util.concurrent.locks.*;
class ticket2 implements Runnable{
private int tick=5;
private String name;
private final ReentrantLock lock=new ReentrantLock();
public void run(){
while(tick>0) {
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");
}
lock.unlock();
}
}
}
public class ThreadDemo7 {
public static void main(String []args){
ticket2 window=new ticket2();
Thread t1=new Thread(window,"first");
Thread t2=new Thread(window,"second");
t1.start();
t2.start();
}
}
完美实现了同步问题
sleep和wait
- sleep和wait都会让出运行权,且都会使得当前线程进入阻塞状态
- sleep属于Thread静态方法,而wait方法属于Object方法
- 定义sleep必须设置时间,而wait则可以不用
- 使用sleep可以使用interrupt方法唤醒,而执行wait方法则使用notify和notifyAll随机取出锁池中的线程唤醒
- 若线程T拥有对象O的对象锁时,执行sleep,线程T将会进入对象O的锁池,但不会释放对象O的锁,而wait,进入锁池后会释放对象O的锁。
小结
终于终于突破100了,这是第100篇博文,很开心,时间:2017年10月28日星期六16点31分,字数,嗯,这是一篇高质量的Java多线程博文,多线程应用在很多工程领域,其中面试也是少不了的一个环节,先mark,以后再来复习