什么是线程?
当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
线程和进程
计算机中运行的任务就对应一个进程。也可以这么说,当一个程序进入了内存中运行时,就变成了一个进程。线程是进程中的执行单元。简单的说就是系统可以同时代的执行多个任务,每个任务就是一个进程,进程可以同时执行多个任务,每个任务就是一个线程。
线程的创建和启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java中有三种创建线程的方法,下面我们一一介绍:
1.继承Thread类创建线程类
一个类继承了Thread类,要重写它的run方法,这个run方法就是代表了线程需要完成的任务,通过创建Thread子类的实例,就创建了线程对象,调用线程对象的start方法来启动该线程。
示范:
public class MyThread extends Thread{ private int count; @Override public void run() { for(; count < 100; count++) { System.out.println(this.getName() + " " + count); } } public static void main(String[] args) { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if(i == 30) { new MyThread().start(); new MyThread().start(); } } } }
这个程序就有三个线程在跑了,一个是main方法这个主线程,两个是我new出来的,上面我用到了两个线程常用的方法:
Thread.currentThread():currentThread是Thread类的静态方法,该方法返回当前正在执行的线程对象。
getName():该方法是Thread类的实例方法,该方法返回调用该方法的线程名字,如果你没有给你的线程取名字的话,就默认从thread-0、thread-1这样数下去
我们来看看这个程序执行的结果:
从这个程序很明显的看出来,这些线程都是并发的,cpu轮流去执行,画了红线的地方可以看出,Thread-0和Thread-1两个线程输出的count是不连续的,也就是说明这个两个线程不是共享实例属性的,就是说每次new出一个实例,都有自己的一份count,说明这个创建线程的方法不能实现线程的资源共享。怎么才能达到共享呢,看看第二个方法吧!
2.实现Runnable接口创建线程类
写了一个类,让这个类就实现Runnable接口,然后就需要重写run方法,这个run方法跟前面的一样,是线程的执行体。创建Runnable实现类的实例,用这个实例作为Thread的target类创建Thread对象,该Thread对象真正的线程对象。看看下面的示范先吧:
public class RThread implements Runnable{ private int count; @Override public void run() { for(; count < 100; count++) { System.out.println(Thread.currentThread().getName() + " " + count); } } public static void main(String[] args) { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if(i == 30) { RThread rThread = new RThread(); new Thread(rThread, "one").start(); new Thread(rThread, "two").start(); } } } }
我们来看看结果:
从这个结果可以看出,count是连读的,也就是说count被这两个线程共享了,这里创建的Runnable对象是作为线程对象的target,可以这么的理解,通过这个target,new出来的线程都是共享这个target的资源,这样就实现了多线程共享了。这里有一个Thread的重载构造方法,里面一个参数是target,另一个是线程的名字,这样就可以设置我们线程的名字,不用用默认那个这么难听的名字了。
3.使用Callable和Future创建线程
Java提供了Callable接口,这个接口有个call方法作为线程执行体,但这个call方法比run方法更加的强大,可以有返回值,也可以声明抛出异常,Java还提供了Future接口来代表Callable接口里的call方法的返回值,并为Future提供一个FutureTask实现类,这个FutureTask有几个常用的方法:
boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务
V get(long timeout, TimeUnit unit):返回Callable任务里call方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间
boolean isCancelled():如果在Callable任务正常完成前被取消,返回true
boolean isDone():如果Callable任务已完成,返回true
这里需要注意的是Callable接口有泛型限制,Callable接口里的泛型参数类型与call方法的返回值类型相同。
看看下面的用法你就很容易知道了:
public class CallableThread implements Callable<String>{ @Override public String call() throws Exception { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } return "调用完毕"; } public static void main(String[] args) throws Exception { CallableThread ct = new CallableThread(); FutureTask<String> task = new FutureTask<String>(ct); for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread() + " " + i); if(i == 20) { new Thread(task, "ct").start(); } } System.out.println(task.get()); } }
线程的生命周期:
线程的生命周期有下面几个方面:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
当我们new一个线程类的时候,跟平时我们new出来的东西是一致的,只有启动了start方法,这种线程才进入就绪的状态,但是调用了start方法不就是调用了run方法,run方法的调用是取决于JVM里线程调度器的调度。
线程的运行就不用说了,这个阻塞的状态可能是线程在等待一个IO的输入,或者一个通知。
当run方法或者call方法执行完成后,线程就结束了,也可以直接调用线程的stop方法来结束线程,但是这个方法很容易导致死锁,所有不推荐使用。
控制线程:
Java提供了一些便捷的方法让我们很好地控制线程的执行。
join线程
Thread提供了让一个线程等待另外一个线程完成的方法--Join方法,当某个程序执行中使用其他线程的join方法时,调用线程将被阻塞,知道被join方法加入的join线程执行完为止。
看看下面的例如就知道了:
public class JoinThread implements Runnable{ @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) throws InterruptedException { for(int i = 0; i < 100; i++) { if(i == 20) { JoinThread joinThread = new JoinThread(); Thread thread = new Thread(joinThread, "MyThread"); thread.start(); //main线程调用了thread线程的join方法,就是说要等MyThread这个线程执行完才可以执行 thread.join(); } System.out.println(Thread.currentThread().getName() + " " + i); } } }
后台线程
有一种线程,它是工作在后台的,是为其他线程提供服务的,这个称为“后台线程(Daemon Thread)”,又称为“守护线程”,JVM的垃圾回收机制就是典型的后台线程。
后台线程有个特点:如果所有前台的线程都死亡了,后台线程会自动死亡。调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。
public class DaemonThread implements Runnable{ @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) throws InterruptedException { DaemonThread dt = new DaemonThread(); Thread thread = new Thread(dt, "DaemonThread"); thread.setDaemon(true); thread.start(); for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
大你跑这个程序的时候就会发现run方法中的i是不能达到最大值的,因为main线程结束的时候后台线程也就结束了,这就是后台线程的特征。
线程睡眠:sleep
线程睡眠Thread的静态方法sleep是线程进入睡眠状态,就是暂停了,sleep方法里面的参数写的就是睡眠的毫秒数,看看例子就是秒懂了。
public class SleepThread { public static void main(String[] args) throws InterruptedException { for(int i = 0; i < 100; i++) { if(i == 20) { System.out.println("暂停中"); Thread.sleep(3000); System.out.println("继续"); } System.out.println(Thread.currentThread().getName() + " " + i); } } }
线程安全问题:
在多线程运行的时候很容易出现线程安全的问题,什么是线程安全的问题呢,拿取钱这个问题来说说吧,可能你是进去一个语句中判断你的钱是否足够的时候,过了,然后跳到另外一个语句中,准备将钱减少,但是还没有执行,这个时候另外一个线程进来了,一判断,也可以,也取钱了,这样的做法是不行的,这个就是多个线程访问的时候容易出现的后果。
同步代码块:
为了解决这个问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的方法就是同步代码块:
synchronized(obj) { //同步代码块 }
上面就是说:线程开始执行同步代码块之前,必须先获取对同步代码块监视器的锁定,在任何时候,只有一个线程能够获得同步监视器的锁定,只有当这个线程执行完成后,才会释放这个同步监视器的锁定。
虽然Java程序允许使用任何对象作为同步监视器,但是合理的监视对象应该是那个可能发生并发访问的资源作为同步监视器。
public class SynchronizedCode { public static void main(String[] args) { ATM atm = new ATM(); new Thread(atm, "One").start(); new Thread(atm, "Two").start(); } } class ATM implements Runnable{ private int money = 100; public void run() { synchronized (Integer.valueOf(money)) { if(money > 80) { money -= 80; System.out.println("取钱成功"); }else { System.out.println("取钱失败"); } } } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } }
同步方法
与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法使用了synchronized关键字来修饰方法,对同步方法来说,无需指定同步监视器的对象,这个同步方法监视的对象时this,也就是这个类本身。
这个方法的使用比较简单,就不多加演示了。
同步锁
Java提供了一种功能更加强大的线程同步机制,通过显式定义同步锁对象来实现同步,Java使用Lock对象来实现,Lock是个接口,它有实现类ReentrantLock,通过这个类的lock方法我们可以加锁,通过unlock方法我们可以解锁,看看下面的例子就知道了:
public class LockThread { private final ReentrantLock lock = new ReentrantLock(); public void method() { //获得锁 lock.lock(); try { //代码块 }catch(Exception e) { e.printStackTrace(); }finally { //释放锁 lock.unlock(); } } }
死锁
什么是死锁问题?很简单,就是两个线程在互相等待对象释放同步监视器,这样就会发生死锁,使程序无法进行下去。
class A { public synchronized void print(B b) { System.out.println(Thread.currentThread().getName() + " In print "); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " is ready to use B"); b.use(); } public synchronized void use() { System.out.println("use A"); } } class B { public synchronized void print(A a) { System.out.println(Thread.currentThread().getName() + " In print "); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " is ready to use A"); a.use(); } public synchronized void use() { System.out.println("use B"); } } public class DeadThread implements Runnable{ A a = new A(); B b = new B(); public void useA() { Thread.currentThread().setName("Main Thread"); a.print(b); } @Override public void run() { Thread.currentThread().setName("Other Thread"); b.print(a); } public static void main(String[] args) { DeadThread deadThread = new DeadThread(); new Thread(deadThread).start(); deadThread.useA(); } }
从这个例子就很清楚看到死锁的问题
两者都在等待着对方,可是都在加着锁,无法进入对方。
线程通信
多线程在系统运行的时候,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换,但我们可以通过一些机制来保证线程协调运行。
传统的线程通信
wait():让当前的线程等待
notify():唤醒等待线程中的一个
notifyAll():唤醒所有等待中的线程
public class TraditionalConversation implements Runnable{ private int flag = 1; public synchronized void print() { try { if(flag == 1) { flag = 0; System.out.println("wait begin"); //wait方法就是让当前的线程等待,当被notify的时候才被激活 wait(); System.out.println("wait end"); }else { flag = 1; System.out.println("notify begin"); Thread.sleep(3000); //唤醒一个正在wait的线程,这个方法只能唤醒一个,若要唤醒全部的话,可以使用notifyAll notify(); System.out.println("notify end"); } }catch(Exception e) { e.printStackTrace(); } } public void run() { print(); } public static void main(String[] args) { TraditionalConversation tc = new TraditionalConversation(); new Thread(tc, "One").start(); new Thread(tc, "Two").start(); } }
使用Condition控制线程通信
当使用Lock对象的方法来控制线程同步问题的时候,我们可以对应使用Condition,这个对象的await方法对应wait方法,signal方法对应notify方法。
public class ConditionConversation implements Runnable{ private int flag = 1; //获取lock对象 private final Lock lock = new ReentrantLock(); //获取Condition对象 private final Condition condition = lock.newCondition(); public void print() { lock.lock(); try { if(flag == 1) { flag = 0; System.out.println("await begin"); //await方法就是让当前的线程等待,当被signal的时候才被激活 condition.await(); System.out.println("await end"); }else { flag = 1; System.out.println("signal begin"); Thread.sleep(3000); //唤醒一个正在await的线程,这个方法只能唤醒一个,若要唤醒全部的话,可以使用signal condition.signal(); System.out.println("signal end"); } }catch(Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void run() { print(); } public static void main(String[] args) { ConditionConversation cc = new ConditionConversation(); new Thread(cc, "One").start(); new Thread(cc, "Two").start(); } }
线程池
线程池跟数据库的连接池是相似的,在系统启动的时候就创建好空闲的线程,当程序将一个Runnable或者Callable对象传给线程池,线程池就会启动这个对象的run方法或者call方法,方法执行完后,不会死亡,而是再次返回线程中成为空闲状态。
Java中提供了Executors工厂类来产生线程池,一个ExecutorService对象就是一个线程池,下面我们来看看他的使用:
public class ThreadPool implements Runnable{ @Override public void run() { for(int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { ThreadPool tp = new ThreadPool(); //创建一个线程池,包含五个空闲线程 ExecutorService pool = Executors.newFixedThreadPool(5); //向线程池提交2个线程 pool.submit(new Thread(tp)); pool.submit(new Thread(tp)); //关闭线程池 pool.shutdown(); } }