多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
下面通过示意图来说明这两个步骤:
1、如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
2、从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b,load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
继承Thread类。
实现步骤:
1、继承Thread并且重写Thread的run(),将线程运行的代码实现在run方法里。
2、创建类的实例。
3、调用start()方法是线程使线程进入就绪状态,等待cpu分配资源。
注意:如果直接调用这个对象的run方法,这时底层资源并没有完成资源的创建和请求分配,仅仅是简单的对象调用。
代码示例:
public class ThreadTest { public static void main(String[] args) { //实例化一个自定义线程并启动 //注意,此时线程处于 new MyThread().start(); //主线程中打印输出 for (int i = 0; i < 10; i++) { //获取当前线程的名称 System.out.println(Thread.currentThread().getName() + " --> " + i); } } } //创建自定义线程,继承Thread父类 class MyThread extends Thread { @Override //覆写父类的run()方法,从而实现自己的业务逻辑 public void run() { for (int i = 0; i < 10; i++) { //Thread.currentThread().getName() 获取当前线程的名称 System.out.println(Thread.currentThread().getName() + " --> " + i); } } }
实现Runnable。
实现步骤:
1、实现Runnable并且重写Runnable的run(),将线程运行的代码实现在run方法里。
2、创建类的实例。
3、将上述的示例传入Thread的构造函数来创建Thread实例;
调用Thread实例的start()方法是线程使线程进入就绪状态,等待cpu分配资源。
代码示例
public class ThreadTest { public static void main(String[] args) { //实例化一个自定义线程并启动 //注意,此时线程处于 MyThread myThread = new MyThread(); new Thread(myThread).start(); //主线程中打印输出 for (int i = 0; i < 10; i++) { //获取当前线程的名称 System.out.println(Thread.currentThread().getName() + " --> " + i); } } } //创建自定义线程,继承Thread父类 class MyThread implements Runnable { @Override //覆写父类的run()方法,从而实现自己的业务逻辑 public void run() { for (int i = 0; i < 10; i++) { //Thread.currentThread().getName() 获取当前线程的名称 System.out.println(Thread.currentThread().getName() + " --> " + i); } } }
两种创建的区别
区别:
推荐实现Runnable 接口。
java中的线程主要分为:用户线程和守护线程:
1、一般java代码默认创建的线程为用户线程。
2、守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。
java中的守护线程,在其运行之前将Thread实例设置了setDaemon(true)
守护线程示例:
//守护线程设置 thread.setDaemon(true); // 准备就绪 等待运行 thread.start();
区别:
守护线程和用户线程的唯一区别是:当进程中没有活动的用户线程时,守护线程会被jvm中断,退出程序
线程睡眠——sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
public class Test1 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { System.out.println("main" + i); Thread.sleep(100); } } }
线程让步——yield
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。
代码示例:
class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("低级", 1).start(); new MyThread("中级", 5).start(); new MyThread("高级", 10).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);// 设置线程的名称 this.setPriority(pro);// 设置优先级 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } } }
线程合并——join
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
它有3个重载的方法:
代码示例:
public class Test1 { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); thread.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度 for (int i = 0; i < 30; i++) { System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!"); } } } class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } }
线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10
代码示例:
public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("高级", 10).start(); new MyThread("低级", 1).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);//设置线程的名称 setPriority(pro);//设置线程的优先级 } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } }
守护线程
setDaemon方法的详细说明:
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)
代码示例:
//守护线程设置 thread.setDaemon(true); // 准备就绪 等待运行 thread.start();
如何结束一个线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用interrupt结束一个线程。
代码示例:
public class Test1 { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); } } class MyThread extends Thread { int i = 1; @Override public void run() { while (true) { System.out.println(i); System.out.println(this.isInterrupted()); try { System.out.println("我马上去sleep了"); Thread.sleep(2000); this.interrupt(); } catch (InterruptedException e) { System.out.println("异常捕获了" + this.isInterrupted()); return; } i++; } } }
线程的等待与唤醒
wait():
等待对象的同步锁,需要获得该对象的同步锁才可以调用这个方法,否则编译可以通过,但运行时会收到一个异常:IllegalMonitorStateException。
调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。
notify():
唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
notifyAll():
唤醒所有等待的线程,注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。
其他一些方法:
图示:
1、新建状态
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。
2、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
3、运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
4、阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:
5、死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
原因:
其他线程在一个线程将本地内存中变量写入准内存之前读取了这个变量,导致错误。
代码示例(售票问题):
public class SellTicket { public static void main(String[] args) { SaleTicket st = new SaleTicket(); //创建四个线程买票 Thread t1 = new Thread(st, "一号窗口"); Thread t2 = new Thread(st, "二号窗口"); Thread t3 = new Thread(st, "三号窗口"); Thread t4 = new Thread(st, "四号窗口 "); t1.start(); t2.start(); t3.start(); t4.start(); } } class SaleTicket implements Runnable { private int tickets = 100; public void run() { //当票量大于0时,继续买票 while (tickets > 0) { System.out.println(Thread.currentThread().getName() + "卖出 第 " + (tickets--) + "张票"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
原因:
即由于两个或多个线程都无法得到相应的锁而造成的两个线程都等待的现象。这种现象主要是因为相互嵌套的synchronized代码段而造成,因此,在程序中尽可能少用嵌套的synchronized代码段是防止线程死锁的好方法。
代码示例:
public class DeadLock { //水壶 private Object object1 = new Object(); //水杯 private Object object2 = new Object(); public static void main(String[] args) { new DeadLock().test(); } private void test() { // TODO Auto-generated method stub //people 1 Thread th1 = new Thread(new Dead(0), "小明"); //people 2 Thread th2 = new Thread(new Dead(1), "小华"); th1.start(); th2.start(); try { Thread.sleep(6000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //死锁类 class Dead implements Runnable { private int tag = 0; public Dead(int _tag) { tag = _tag; // TODO Auto-generated constructor stub } @Override public void run() { // TODO Auto-generated method stub if (tag == 0) { //尝试着拿水壶 synchronized (object1) { System.out.println(Thread.currentThread().getName() + "拿到了水壶"); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //尝试着去拿水杯 System.out.println(Thread.currentThread().getName() + "尝试着拿水杯"); synchronized (object2) { System.out.println(Thread.currentThread().getName() + "也拿到了水杯"); } } } else { //尝试着拿水杯 synchronized (object2) { System.out.println(Thread.currentThread().getName() + "拿到了水杯"); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //尝试着去拿水壶 System.out.println(Thread.currentThread().getName() + "尝试着拿水壶"); synchronized (object1) { System.out.println(Thread.currentThread().getName() + "也拿到了水壶"); } } } } } }
Java 语言提供了两个关键字:synchronized 和volatile,一个对象ReetrantLock来实现线程的同步。
synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
给某个对象加锁:
//object必须为要同步的对象,也就是可能会出现问题的对象 synchronized (object){ //业务逻辑 }
修饰一个方法:
写法一:
public synchronized void method () { // todo }
写法二:
public void method () { synchronized (this) { // todo } }
修饰静态方法:
public synchronized static void method () { // todo }
修饰类:
class ClassName { public void method() { synchronized (ClassName.class) { // todo } } }
要点:
volatile
代码示例:
class Bank { //需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
注意:
Volatile Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错,所以尽量使用synchronized同步
ReetrantLock
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
ReetrantLock 是在代码的任何地方都可以获得锁、释放锁、但是为了安全推荐在finally块中释放锁。
代码示例:
Lock lock = new ReentrantLock(); lock.lock(); try { // update object state } finally { lock.unlock(); }
1、 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
2、线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
1. new SingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.new FixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. new CachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4.new ScheduledThreadPool
创建一个执行延时、周期行执行任务的线程池。
ExecutorService表述了异步执行的机制,并且可以让任务在后台执行。
ExecutorService对象一般使用流程:
1、Executors创建一个线程池
2、调用submit 添加线程任务(参数是一个实现Runnable接口的对象)。(周期线程:schedule、scheduleAtFixedRate、scheduleWithFixedDelay)
3、调用shutdown等任务完成后关闭线程池。
代码示例:
//单线程池 ExecutorService executorService= Executors.newSingleThreadExecutor(); executorService.submit(new SayHello()); executorService.submit(new SayHello()); executorService.submit(new SayHello());
//固定大小的线程池 ExecutorService executorServiceFixed=Executors.newFixedThreadPool(4); executorServiceFixed.submit(new SayHello()); executorServiceFixed.submit(new SayHello()); executorServiceFixed.submit(new SayHello()); executorService.shutdown();
//可变的(缓存)线程池 ExecutorService executorServiceCache=Executors.newCachedThreadPool(); executorServiceCache.submit(new SayHello()); executorServiceCache.submit(new SayHello()); executorServiceCache.submit(new SayHello()); executorServiceCache.submit(new SayHello());
//周期性线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
//延迟的调用 scheduledExecutorService.schedule(new SayHello(), 2, TimeUnit.SECONDS); System.out.println(new Date());
//固定延时的线程池。 //long initialDelay 初始化的延迟时间:提交任务X(TimeUnit)后开始执行此任务。 //long delay 周期性的延迟时间:执行完成一个任务后的X(TimeUnit)后再执行下一个任务,时间间隔是以完成任务时的时间点。 //本次任务完成的时间点+delay(TimeUnit)=下次任务开始的时间。 scheduledExecutorService.scheduleWithFixedDelay(new SayHello(), 2, 4, TimeUnit.SECONDS); System.out.println(new Date());
//固定执行间隔的线程池 //long delay 周期性的执行时间:开始执行一个任务后的X(TimeUnit)后再执行下一个任务,时间间隔是以开始任务时的时间点。 //本次任务开始的时间点+delay(TimeUnit)=下次任务开始的时间。但是:如果delay小于执行任务的执行时间的时候,第二个任务会在第一个任务完成后开始。 scheduledExecutorService.scheduleAtFixedRate(new SayHello(), 3, 4, TimeUnit.SECONDS); System.out.println(new Date());