Java并发和多线程基础(一)

1.java线程状态

Java中的线程可以处于下列状态之一:

  • NEW: 至今尚未启动的线程处于这种状态。
  • RUNNABLE: 正在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED: 受阻塞并等待某个监视器锁的线程处于这种状态。
  • WAITING: 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
  • TIMED_WAITING: 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
  • TERMINATED: 已退出的线程处于这种状态。

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。

1.1 New

Thread t = new MyThread(r);后线程处于new状态。

1.2 Runnable

调用start方法后,线程处于Runnable状态。一个Runnable(可运行)的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

1.3 Blocked

一个线程试图获取一个内部的对象锁(不是java.util.concurrent中的锁),而该锁被其他线程持有的时候,该对象进入阻塞状态(Blocked)。

1.4 waiting

某一线程因为调用下列方法之一而处于等待状态:

  • 不带超时值的 Object.wait
  • 不带超时值的 Thread.join
  • java.util.concurrent中的Lock或Condition

1.5 timed_waiting

某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态:

  • Thread.sleep
  • 带有超时值的 Object.wait
  • 带有超时值的 Thread.join
  • java.util.concurrent中的Lock.tryLock以及Condition.await的计时版

1.6 terminated

线程被终止的两种原因:

  • run方法正常退出而自然终止
  • 因为一个没有捕获的异常终止了run方法而意外终止

2.同步(一些比较底层的解决方案)

同步有两方面的含义:互斥与内存可见性

  • 2.1 锁对象Lock和条件对象Condition
  • 2.2synchronized关键字
  • 2.3volatile域
  • 2.4原子类
  • 2.5ThreadLocal

2.1 锁对象Lock和条件对象Condition

Java中为了方便程序员编写并发代码引入了synchronized关键字。为了深刻理解synchronized关键字代表的含义就必须得先知道两个概念:锁对象 和 条件对象。

2.1.1 锁对象

引入锁对象是为了确保任何时候只有一个线程能访问临界区。使用Lock保护代码的基本结构如下:

myLck.lock();
try{
    critical section
}
finally{
    myLock.unlock();
}

这种结果确保了任何时刻只有一个线程进入临界区。一旦一个线程持有了该锁对象,任何其他线程都无法通过lock语句。当其他其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。

使用一个锁来保护Bank类的transfer方法的例子:

public class Bank {
    private Lock bankLock = new ReentrantLock();
    //...

    public void transfer(int from, int to,int amount){
        bankLock.lock();
        try{
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
        }
        finally{
            bankLock.unlock();
        }
    }

}

可重入锁?

上述Bank例子中的锁是可重入的。可重入锁可参考这儿

2.1.2 条件对象

为什么需要条件对象?

上述Bank例子演示了从一个银行账户向另一个银行账户转账的过程。但上述内部转账流程仍需完善:如果转出账户的余额不够的话,应该等待其他账户转给自己后再执行上述转账操作。

public void transfer(int from, int to,int amount){
        bankLock.lock();
        try{
            while (accounts[from] < amount) {
                // wait
                ...
            }
            // transfer funds
            ...
        }
        finally{
            bankLock.unlock();
        }
    }

如果转出账户的余额不够的话,应该等待其他账户转给自己后再执行上述转账操作。但是,当前线程刚刚获得了对bankLock的排他性访问,因此别的线程不可能有执行该transfer方法的机会。怎么办呢?解决方案就是让当前线程释放bankLock锁对象并且等待账户余额满足条件后继续执行。条件对象可以帮我们解决这个问题,这就是为什么我们需要条件对象的原因。

一个锁可以有一个或者多个相关的条件对象。可以使用Lock对象的newCondition()实例方法获取一个条件对象。

调用Condition对象的await()方法,会使得当前线程进入该条件的等待集。

调用Condition对象的signalAll()方法,会使得所有等待在该条件对象上的进程被唤醒。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Bank {
    private Lock bankLock = new ReentrantLock();
    private Condition sufficientFunds = bankLock.newCondition();
    private final double[] accounts;

    public Bank(int n,double initialBalance){
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
    }

    public void transfer(int from, int to,int amount) throws InterruptedException{
        bankLock.lock();
        try{
            while (accounts[from] < amount)
                sufficientFunds.await();
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" %10.2f from %d to %d",amount,from,to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
            sufficientFunds.signalAll();

        }
        finally{
            bankLock.unlock();
        }
    }

    public double getTotalBalance() {
        bankLock.lock();
        try{
            double sum = 0;
            for (double d : accounts) {
                sum += d;
            }
            return sum;
        }
        finally{
            bankLock.unlock();
        }
    }

}

注意:对await的调用应该在如下循环体中:

while(!(ok to proceed))
    condition.await();

总结:

  • 锁用来保护代码片段,任何时刻只有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 锁一个拥有一个或者多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

2.2 synchronized关键字

从1.0版开始,java中的每个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么该方法所属对象的锁将保护整个方法。换句话说,要调用该方法,线程必须获得内部的对象锁。

换句话说,

public synchronized void method(){
    method body
}

等价于

public void method(){
    this.intrinsicLock.lock();
    try{
        method body
    }
    finally{
        this.intrinsicLock.unlock();
    }
}

内部对象锁只有一个相关的条件对象。Object类中的wait方法添加一个线程到等待集中,notify/notifyAll方法解除等待线程的阻塞状态。调用wait或notifyAll等价于

intrinsicLock.await();
intrinsicLock.signalAll();

理解了wait,notify/notifyAll等方法的内部等价形式就很容易明白为什么对wait,notify/notifyAll等方法的调用只能在同步控制方法或者同步控制块里面使用。

面试题:Java中sleep和wait的区别?

  • 来源:sleep来自Thread类,和wait来自Object类。
  • 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  • 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。

synchronized版的Bank类如下:

public class Bank {

    private final double[] accounts;

    public synchronized void transfer(int from, int to,int amount) throws InterruptedException{
        while (accounts[from] < amount)
            wait();
        System.out.println(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d",amount,from,to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
        notifyAll();
    }

    public synchronized double getTotalBalance() { ... }
}

2.3volatile域

同步包含两方面:互斥和内存可见性。volatile修饰的Field保证了内存可见性,但不保证互斥(原子性)。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

2.4 原子类

java.util.concurrent.atomic包中有许多类使用了很高效的机器级指令(而不是使用锁)来保证其操作的原子性。

应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。

2.5 ThreadLocal类

有时候可能要避免共享变量,使用ThreadLocal类为各个线程提供各自的实例。

ThreadLocal类包装的字段通常是static变量,否则如果是实例变量则完全没必要。

package think.in.java.chap21.section3;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * private ThreadLocal<Integer> value = new ThreadLocal<Integer>();
 * 该列子说明:
 * 如果去掉static,则每个ThreadLocalVariableHolder对象有一个value域,并且访问该对象的每个线程有一个本地value变量。
 *
 */
public class ThreadLocalVariableHolder extends Thread{
    private ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
        private Random random = new Random(47);
        protected synchronized Integer initialValue(){
            return random.nextInt(10000);
        }
    };
    public void increment() {
        value.set(value.get()+1);

    }
    public int get() {
        return value.get();
    }
    public void run(){
        increment();
        System.out.println(Thread.currentThread()+" | "+this);
    }
    public String toString() {
        return "["+get()+",value=" + value + "]";
    }
    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 2; i++) {
            ThreadLocalVariableHolder t = new ThreadLocalVariableHolder();
            for (int j = 0; j < 4; j++) {
                exec.execute(t);
            }
        }
        TimeUnit.SECONDS.sleep(3);
        exec.shutdownNow();
    }
}

3. 阻塞队列BlockingQueue(同步的高层解决方案)

前面介绍了java并发编程的底层构件块,实际编程中应该尽量远离底层结构。多线程中的许多问题都是生产者-消费者模型,可以通过一个或者多个队列将其优雅地形式化。从5.0开始,JDK在java.util.concurrent包里提供了阻塞队列的官方实现。

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。

BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(nullfalse,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:

  抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用

BlockingQueue 不接受 null 元素。试图 addputoffer 一个 null 元素时,某些实现会抛出 NullPointerExceptionnull 被用作指示 poll 操作失败的警戒值。

有已知实现类:
ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue

时间: 2024-10-19 17:19:40

Java并发和多线程基础(一)的相关文章

1-3 Java并发与多线程基础

1.并发与多线程简介 最初计算机是单任务的,后来发展到可以并行运行多任务(进程),由操作系统来调度,每个任务可以获得一个时间片.多任务下,每个任务在使用系统资源结束后需要释放资源给其他任务. 后来,同一个任务内部发展出多个线程并发操作,会对相同的内存空间进行并发读写操作.更现代的计算机伴随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行.有些在多线程中出现的问题会和多任务以及分布式系统中出现的存在类似,因此该系列会将多任务和分布式系统方面作为参考,所以叫法上称为"

JAVA并发总结-基础篇

多线程 1. java中有几种方法可以实现一个线程? 继承Thread类,实现Runnable接口创建一个线程的唯一方法是实例化java.lang.Thread类(或其子类),并调用其start()方法 2. 如何停止一个正在运行的线程? 调用ThreadInstanceA.inerrupt()方法,这样当A线程在Thread的sleep,join方法,或者Object的wait方法的时候会直接抛出InerruptedException,捕捉后便可退出. public void shutdown

Java 并发和多线程(一) Java并发性和多线程介绍[转]

作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行. 随着多任务对软件开发者带来的

Java并发和多线程2:3种方式实现数组求和

本篇演示3个数组求和的例子. 例子1:单线程例子2:多线程,同步求和(如果没有计算完成,会阻塞)例子3:多线程,异步求和(先累加已经完成的计算结果) 例子1-代码 package cn.fansunion.executorservice; public class BasicCaculator { public static long sum(int[] numbers){ long sum = 0; for(int i=0;i<numbers.length;i++){ sum += numbe

Java并发和多线程1:并发框架基本示例.txt

Executor框架是指java 5中引入的一系列并发库中与executor相关的一些功能类,其中包括ThreadPool,Executor,Executors,ExecutorService,CompletionService,Future,Callable等. 并发编程的一种编程方式是把任务拆分为一系列的小任务,即Runnable,然后在提交给一个Executor执行,Executor.execute(Runnalbe) .Executor在执行时使用内部的线程池完成操作.一.创建线程池Ex

Java并发和多线程那些事儿

我记得我接触电脑的时候是在小学三年级的时候,那是1995年,那年发布了windows95,但是我学习的时候还是只是dos系统,简单对于文件的一些命令操作还有五笔 在过去的那个年代,电脑都是单CPU,也就是单任务处理:多任务处理到后来才慢慢发展起来,多任务代表电脑在同一时刻内可以处理很多并行操作,这样CPU被利用率高了,多个任务都可以共享.多任务处理的出现对于软件开发者来说需要做更多的事,就是资源释放,也就是垃圾回收,在软件不用的时候要释放资源,这样就可以给其他软件腾出资源来使用,就像ios内存机

Java并发和多线程(二)Executor框架

Executor框架 1.Task?Thread? 很多人在学习多线程这部分知识的时候,容易搞混两个概念:任务(task)和线程(thread). 并发编程可以使我们的程序可以划分为多个分离的.独立运行的任务.而这些任务具体得由线程来驱动.Java中,Thread类自身不执行任何操作,它只是驱动赋予它的任务,任务由Runnable接口提供. 2.executor Executor是个简单的接口,但它却提供了一种标准的方法将任务的提交过程与任务的执行过程解耦开来,从而无须太大困难就可以为某种类型的

java并发与多线程API学习

Executor接口 public interface Executor { void execute(Runnable command); } Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类. 在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类.两者都可以被ExecutorService执行,但是Run

JAVA学习总结-多线程基础:

参考书籍:疯狂JAVA讲义 1.进程和线程; 进程是处于运行过程中的程序;并且具有一定的独立功能;进程是系统进行系统资源分配和调度的一个独立单位. 一般而言,进程包括以下三个特征: 独立性:进程是系统中存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间.在没有经过进程本身允许的情况下,一个用户进程不可以访问其他进程的地址空间. 动态性:进程与程序的区别在于,程序是一个静态的指令集合,而进程是一个正在系统中活动的指令集合.在进程中加入了时间的概念,进程具有自己的生命周期和不同