Java多线程详解


Java多线程详解






 

多线程简介

概述

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

 

 

优点

  1. 使用线程可以把占据时间长的程序中的任务放到后台去处理
  2. 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
  3. 程序的运行速度可能加快
  4. 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
  5. 多线程技术在软件开发中有举足轻重的位置。

 

缺点

  1. 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
  2. 更多的线程需要更多的内存空间。
  3. 线程可能会给程序带来更多“bug”,因此要小心使用。
  4. 线程的中止需要考虑其对程序运行的影响。
  5. 通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。

 

 

程序、进程、线程

  1. 程序:程序是指令的集合,它是静态描述文本;程序本身是并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行。比如:我们写的java程序。
  2. 进程:当系统为程序分配资源,程序就可以运行,就产生了进程。也就是进程就是程序的一次执行活动。所以:进程是系统进行资源分配和调度的一个独立单位。示例:启动任务管理器[PID列默认是不显示]、执行一个main函数,然后观察任务管理器。
  3. 线程:也称作轻量级进程,是比进程颗粒度更小的能独立运行的基本单位。是系统资源分配和调度的基本单位。一个进程可以支持多个线程。上述例子中有一个main线程在运行。

 

 


多线程内存模型

模型

 

 

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

 


下面通过示意图来说明这两个步骤:

 


1、如上图所示,本地内存AB有主内存中共享变量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内存模型定义了以下八种操作来完成:

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。



如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行readload操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行storewrite操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是readload之间,storewrite之间是可以插入其他指令的,如对主内存中的变量ab进行访问时,可能的顺序是read aread bload bload aJava内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  1. 不允许readloadstorewrite操作之一单独出现
  2. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(loadassign)的变量。即就是对一个变量实施usestore操作之前,必须先执行过了assignload操作。
  5. 一个变量在同一时刻只允许一条线程对其进行lock操作,lockunlock必须成对出现
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行loadassign操作初始化变量的值
  7. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  8. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行storewrite操作)。

 

 


多线程创建和启动

 

线程创建的两种方法

继承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);
        }
    }
}


两种创建的区别

区别:

  1. 在继承Thread中可以直接使用this来调用thread的方法。比如 this.getName()获取线程名字,而继承Runnable只能先获取当前的进程对象,Thread.currentThread().getName()。
  2. 如果线程类只是实现了Runnable接口,那么该类还可以继承其他类。但是继承了thread就不能继承其它类了。
  3. 实现Runnable。可以多个线程共享一个Runnable target对象的资源,所以非常适合多个相同线程来处理同一份资源。

推荐实现Runnable 接口。

 

 


用户、守护线程

java中的线程主要分为:用户线程和守护线程:

1、一般java代码默认创建的线程为用户线程。

2、守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。

java中的守护线程,在其运行之前将Thread实例设置了setDaemon(true)

 

守护线程示例:

//守护线程设置
thread.setDaemon(true);

// 准备就绪 等待运行
thread.start();


区别:

守护线程和用户线程的唯一区别是:当进程中没有活动的用户线程时,守护线程会被jvm中断,退出程序

 


Thread常用方法

线程睡眠——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个重载的方法:

  1. void join()    : 当前线程等该加入该线程后面,等待该线程终止。    
  2. void join(long millis)    : 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度   
  3. void join(long millis,int nanos)    :  等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  

代码示例: 

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 + "次执行!");
        }
    }
}  


 

守护线程

  1. 守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
  2. 守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。

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. isAlive(): 判断一个线程是否存活。 
  2. activeCount(): 程序中活跃的线程数。 
  3. enumerate(): 枚举程序中的线程。 
  4. currentThread(): 得到当前线程。 
  5. setName(): 为线程设置一个名称。 


 

 

线程的生命周期


图示:



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资源,再次变为就绪状态。

 

当发生如下情况是,线程会从运行状态变为阻塞状态:

  1. 线程调用sleep方法主动放弃所占用的系统资源
  2. 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  3. 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
  4. 线程在等待某个通知(notify)
  5. 程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
  6. 当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

 

4、阻塞状态

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:

 

5、死亡状态

当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

 



线程的同步

同步原因:

  1. 多线程同时操作同一变量的时候会产生脏数据,也就是不是预期的结果。
  2. 多线程可能造成死锁,导致程序崩溃。

 


脏数据

原因:

其他线程在一个线程将本地内存中变量写入准内存之前读取了这个变量,导致错误。

代码示例(售票问题):

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
        }
    }
}


要点:

  1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 
  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。 
  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。



volatile

  1. Volatile:它是一个类型修饰符,线程在获取用volatile声明的变量时,直接从主存读取,修改后直接写入主存。
  2. volatile关键字为域变量的访问提供了一种免锁机制, 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。 
  3. volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

 

代码示例:

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());



      



 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值