Java之多线程
Posted 鹤啸九天-西木
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java之多线程相关的知识,希望对你有一定的参考价值。
一、概念
1、进程(Porcess):
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,每个进程在执行过程中拥有独立的内存单元。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器,由程序、数据和进程控制块三部分组成。程序是指令、数据及其组织形式的描述,而进程是程序的实体。在Java中理解为调用一次main()方法时的整个程序的运行活动。
1)特性:
1>动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的;
2>并发性:任何进程都可以同其他进程一起并发执行;
3>独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
4>异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
2)主要状态:
1>就绪状态(Ready):进程已获得除CPU外所需的资源,万事俱备只欠东风。等待分配到CPU资源,只要分配到CPU时进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2>运行状态(Running):进程占用CPU资源。处于此状态的进程的数目小于等于CPU的数目。在没有其他进程可以执行时(比如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3>阻塞状态(Blocked):由于进程等待某种前置依赖条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
2、线程(Thread):
线程被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。多个线程共享他们所在进程中的某些内存。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
辅助理解进程与线程:
如果把CPU看成是电力,把一台计算机看成一个工厂,把每个进程看成是工厂的每个车间,把每个线程看成是车间里的每个工人。那么:
1、电力(CPU)被分成若干部分分别供各个车间(进程)使用,各车间(进程)之间互不影响;
2、假定该工厂的电表保险丝质量太差,不能让所有车间(进程)同时运转而只能让一个车间(进程)运转。也就是说一个车间(进程)运转时其他车间(进程)必须得歇着,即单个CPU只能运行一个任务,任一时刻CPU总是运行一个进程,其他进程处于非运行状态;
3、车间(进程)中有很有工人(线程),他们协同完成一件任务。
4、车间(进程)的空间(CPU)是工人(线程)共享的,比如许多房间是每个工人(线程)都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
5、可是每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
6、一个防止其他工人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
7、还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
8、这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙没有了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下还是采用这种设计。
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
1)特性:
1>轻量级:线程基本上不拥有系统资源,只拥有一点必不可少的、能保证独立运行的资源。故线程的切换非常迅速且开销小(在同一进程中的)。
2>并发性:线程都可以同其他线程一起并发执行,不管是同进程中的线程还是不同进程中的线程;
3>共享进程资源:在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
2)状态:
1>使用继承Thread类或者实现Runnable接口的方式创建线程后,此时创建的线程就是新建状态;
2>当新建状态的线程调用start()方法后就处于就绪状态(即“可运行状态”);
3>当线程获取到CUP资源后就进入运行状态,在此之后又分5种情况:
3.1>当运行状态的线程的run()方法或main()方法执行结束后,线程就处于终止状态;
3.2>当运行状态的线程调用自身的yield()方法后,意味着主动放弃CPU资源,重新回到就绪状态,与其他就绪状态的线程公平竞争获取CUP资源,系统有可能又把CPU资源再一次分配给该线程;
3.3>当运行状态的线程调用自身的sleep()方法或其他线程调用join()方法后当前线程就处于阻塞状态(不释放资源),直到sleep()或join()方法结束后阻塞的线程才自动进入就绪状态,等待时机获取CPU资源继续执行本线程;
3.4>当运行状态的线程调用obj的wait()方法后会进入等待队列,不保证顺序(会释放资源),进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只能唤醒一个线程,但我们又不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用中,一般都用notifyAll()方法,唤醒所有线程),线程被唤醒后会进入锁池队列,等待获取锁标记。
3.5>当线程刚进入可运行状态,如果发现将要调用的资源被synchronized(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记。这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于锁池队列(先到先得),一旦线程获得锁标记后,才转为可运行状态,等待系统分配CPU时间片;
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
二、线程使用
当 Java 虚拟机启动时会有某用户线程调用某个指定类的 main 方法,这个线程称为主线程,只有在需要多线程的时候才需要创建线程。获取当前运行的线程的方法是Thread.currentThread()。
1、线程的创建方式:
1>继承Thread类
class Thread1 extends Thread
@Override
public void run()
String threadName = Thread.currentThread().getName();
int count = 0;
while (count < 10)
System.out.println(threadName + " 的run()方法中第 " + count + " 次循环输出");
count++;
public class Test1
public static void main(String[] args)
Thread t1 = new Thread1();
t1.setName("线程 1");
t1.start();
Thread t2 = new Thread1();
t2.setName("线程 2");
t2.start();
输出:
线程 1 的run()方法中第 0 次循环输出
线程 2 的run()方法中第 0 次循环输出
线程 1 的run()方法中第 1 次循环输出
线程 2 的run()方法中第 1 次循环输出
线程 1 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 3 次循环输出
线程 2 的run()方法中第 4 次循环输出
线程 2 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 3 次循环输出
线程 2 的run()方法中第 6 次循环输出
线程 1 的run()方法中第 4 次循环输出
线程 1 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 7 次循环输出
线程 1 的run()方法中第 7 次循环输出
线程 2 的run()方法中第 8 次循环输出
线程 2 的run()方法中第 9 次循环输出
线程 1 的run()方法中第 8 次循环输出
线程 1 的run()方法中第 9 次循环输出
2>实现Runnable接口
当使用Runnable接口时,不能直接创建所需类的对象并运行它,必须从 Thread 类的一个实例内部运行它。
class Runnable1 implements Runnable
@Override
public void run()
String threadName = Thread.currentThread().getName();
int count = 0;
while (count < 10)
System.out.println(threadName + " 的run()方法中第 " + count + " 次循环输出");
count++;
public class Test2
public static void main(String[] args)
Runnable r1 = new Runnable1();
Thread t1 = new Thread(r1);
t1.setName("线程 1");
t1.start();
Thread t2 = new Thread(r1);
t2.setName("线程 2");
t2.start();
输出:
线程 1 的run()方法中第 0 次循环输出
线程 2 的run()方法中第 0 次循环输出
线程 1 的run()方法中第 1 次循环输出
线程 2 的run()方法中第 1 次循环输出
线程 1 的run()方法中第 2 次循环输出
线程 1 的run()方法中第 3 次循环输出
线程 1 的run()方法中第 4 次循环输出
线程 1 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 3 次循环输出
线程 1 的run()方法中第 7 次循环输出
线程 1 的run()方法中第 8 次循环输出
线程 1 的run()方法中第 9 次循环输出
线程 2 的run()方法中第 4 次循环输出
线程 2 的run()方法中第 5 次循环输出
线程 2 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 7 次循环输出
线程 2 的run()方法中第 8 次循环输出
线程 2 的run()方法中第 9 次循环输出
3>实现Callable接口
Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到。
class Callable1 implements Callable<String>
@Override
public String call()
String threadName = Thread.currentThread().getName();
int count = 0;
while (count < 10)
System.out.println(threadName + " 的call()方法中第 " + count + " 次循环输出");
count++;
return threadName + "执行完毕";
public class Test3
public static void main(String[] args)
Callable<String> c1 = new Callable1();
FutureTask<String> futureTask1 = new FutureTask<>(c1);
Thread t1 = new Thread(futureTask1);
t1.setName("线程 1");
FutureTask<String> futureTask2 = new FutureTask<>(c1);
Thread t2 = new Thread(futureTask2);
t2.setName("线程 2");
t1.start();
t2.start();
try
String result1 = futureTask1.get();
System.out.println(result1);
String result2 = futureTask2.get();
System.out.println(result2);
catch (InterruptedException e)
e.printStackTrace();
catch (ExecutionException e)
e.printStackTrace();
catch (Exception e)
e.printStackTrace();
输出:
线程 1 的call()方法中第 0 次循环输出
线程 1 的call()方法中第 1 次循环输出
线程 1 的call()方法中第 2 次循环输出
线程 1 的call()方法中第 3 次循环输出
线程 2 的call()方法中第 0 次循环输出
线程 1 的call()方法中第 4 次循环输出
线程 1 的call()方法中第 5 次循环输出
线程 1 的call()方法中第 6 次循环输出
线程 1 的call()方法中第 7 次循环输出
线程 1 的call()方法中第 8 次循环输出
线程 1 的call()方法中第 9 次循环输出
线程 2 的call()方法中第 1 次循环输出
线程 2 的call()方法中第 2 次循环输出
线程 2 的call()方法中第 3 次循环输出
线程 1执行完毕
线程 2 的call()方法中第 4 次循环输出
线程 2 的call()方法中第 5 次循环输出
线程 2 的call()方法中第 6 次循环输出
线程 2 的call()方法中第 7 次循环输出
线程 2 的call()方法中第 8 次循环输出
线程 2 的call()方法中第 9 次循环输出
线程 2执行完毕
2、线程的命名:
一个运行中的线程总是有名字的,名字有两个来源:一种是虚拟机默认给的名字,另一种是用户自定义设置的名字。线程都可以调用setName()方法设置线程名字,也可以调用getName()方法获取线程的名字,连主线程也不例外。在没有指定线程名字的情况下,虚拟机总会为线程指定名字。并且主线程的名字总是main,非主线程的名字不确定。
3、线程的分类:
1>用户线程(User Thread):
又称前台线程,当有用户线程运行时,JVM不能关闭。当没有用户线程运行时,不管有没有守护线程,JVM都会自动关闭。 默认情况下创建的线程都是用户线程。
2>守护线程(Daemon Thread):
又称后台线程,守护线程的作用是为其他线程的运行提供便利服务的,比如垃圾回收线程就是一个很称职的守护者。守护线程一般是由操作系统创建,当然也可以由用户自己创建。在创建完线程之后,在调用start()方法前调用setDaemon(true)方法将该线程设置为守护线程,调用isDaemon()可以判断该线程是否是守护线程。
4、线程的优先级:
每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程,优先级高的线程会获得较多的运行机会。当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种优先级规则。
Java线程的优先级用整数表示,取值范围是1~10,Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。Thread类有以下三个静态常量:
1>static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10;
2>static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1;
3>static int NORM_PRIORITY: 分配给线程的默认优先级,取值为5。
5、线程的常用方法:
1>public void start()
可以让新建线程变为可运行线程,即准备执行线程中的run()方法。
2>public static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
3>public static void sleep(long millis,int nanos)
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)。
注:①线程睡眠是帮助所有线程获得运行机会的最好方法。
②线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此sleep()方法不能保证该线程睡眠到期后就开始执行。
4>public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。作用是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。但实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
5>public final void join()
等待正在运行的线程终止。让一个线程B“加入”到另外一个线程A的尾部。只有在A执行完毕之后B才能工作。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行
6>public final void join(long millis)
等待正在运行的线程终止的时间最长为 millis
毫秒,如果超过这个时间,则停止等待,变为可运行状态。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行
7>public final void join(long millis,int nanos)
等待正在运行的线程终止的时间最长为 millis
毫秒 +nanos
纳秒,如果超过这个时间,则停止等待,变为可运行状态。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行
6、阻塞队列:
1>阻塞队列:java.util.concurrent.BlockingQueue接口,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Test
public static void main(String[] args)
BlockingQueue<Integer> bqueue = new ArrayBlockingQueue<>(5);
try
for (int i = 0; i < 10; i++)
System.out.println("即将向阻塞队列中添加元素:" + i);
// 将指定元素添加到此队列中,如果没有可用空间,将一直阻塞等待
bqueue.put(i);
System.out.println("向阻塞队列中添加元素:" + i + " 成功");
catch (InterruptedException e)
e.printStackTrace();
System.out.println("程序到此运行结束,即将退出...");
输出:
即将向阻塞队列中添加元素:0
向阻塞队列中添加元素:0 成功
即将向阻塞队列中添加元素:1
向阻塞队列中添加元素:1 成功
即将向阻塞队列中添加元素:2
向阻塞队列中添加元素:2 成功
即将向阻塞队列中添加元素:3
向阻塞队列中添加元素:3 成功
即将向阻塞队列中添加元素:4
向阻塞队列中添加元素:4 成功
即将向阻塞队列中添加元素:5
2>双端阻塞队列:java.util.concurrent.BlockingDeque接口,一个指定长度的队列,两端都可以进出。如果栈满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当栈为空时候,请求栈元素的操作同样会阻塞等待,直到有可用元素为止。
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class Test
public static void main(String[] args)
BlockingDeque<Integer> bdequeue = new LinkedBlockingDeque<>(5);
try
for (int i = 0; i < 10; i++)
if (i % 2 == 0)
System.out.println("即将向双端阻塞队列队首添加元素:" + i);
// 将指定元素添加到此队列队首,如果没有可用空间,将一直阻塞等待
bdequeue.putFirst(i);
System.out.println("向双端阻塞队列队首添加元素:" + i + " 成功");
else
System.out.println("即将向双端阻塞队列队尾添加元素:" + i);
// 将指定元素添加到此队列队尾,如果没有可用空间,将一直阻塞等待
bdequeue.putLast(i);
System.out.println("向双端阻塞队列队尾添加元素:" + i + " 成功");
catch (InterruptedException e)
e.printStackTrace();
System.out.println("程序到此运行结束,即将退出...");
输出:
即将向双端阻塞队列队首添加元素:0
向双端阻塞队列队首添加元素:0 成功
即将向双端阻塞队列队尾添加元素:1
向双端阻塞队列队尾添加元素:1 成功
即将向双端阻塞队列队首添加元素:2
向双端阻塞队列队首添加元素:2 成功
即将向双端阻塞队列队尾添加元素:3
向双端阻塞队列队尾添加元素:3 成功
即将向双端阻塞队列队首添加元素:4
向双端阻塞队列队首添加元素:4 成功
即将向双端阻塞队列队尾添加元素:5
三、线程同步与锁
如果多个线程同时访问并修改同一个对象以及它的成员变量时,由于多个线程同时都在修改数据,会造成数据不正确。如:
import lombok.Data;
@Data
public class BankAccount
/**
* 身份证号/客户号
*/
private String id;
/**
* 姓名
*/
private String name;
/**
* 金额
*/
private Long amount;
/**
* 存款
*
* @param amount 存款金额
*/
public void save(Long amount)
this.amount = this.amount + amount;
System.out.println("打生活费:" + amount + ",打款后账户余额:" + this.amount);
/**
* 取款
*
* @param amount 取款金额
*/
public void withdraw(Long amount)
if (this.amount < amount)
System.out.println("取生活费:" + amount + ",取款前账户余额:" + this.amount + ",余额不足");
return;
this.amount = this.amount - amount;
System.out.println("取生活费:" + amount + ",取款后账户余额:" + this.amount);
public class Parent extends Thread
/**
* 银行账户
*/
private BankAccount bankAccount;
public Parent(BankAccount bankAccount)
this.bankAccount = bankAccount;
@Override
public void run()
for (int i = 1; i <= 12; i++)
this.bankAccount.save(1000L);
public class Child extends Thread
/**
* 银行账户
*/
private BankAccount bankAccount;
public Child(BankAccount bankAccount)
this.bankAccount = bankAccount;
@Override
public void run()
for (int i = 1; i <= 12; i++)
this.bankAccount.withdraw(1000L);
public class Test
public static void main(String[] args)
BankAccount bankAccount = new BankAccount();
bankAccount.setId("000001");
bankAccount.setName("张三");
bankAccount.setAmount(0L);
Parent parent = new Parent(bankAccount);
Child child = new Child(bankAccount);
parent.start();
child.start();
输出:
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:2000
取生活费:1000,取款后账户余额:1000
取生活费:1000,取款后账户余额:0
可以看出输出的最后5条结果是不合理的,连续打款2次后余额仍然是1000。原因是两个线程不加控制的访问Foo对象并修改其数据所致。如果要保证结果的正确合理性,需要达到一个要求,那就是将对Bean实例或num的访问加以限制,每次只能有一个线程在访问。这样就能保证Bean对象中数据的合理性了。
1、锁的概念:Java中每个对象都有一个内置锁,一个对象只有一个锁(更确切地说应该包括“锁”和“钥匙”)。所以如果A线程获得该对象的锁(更确切地说应该是把该对象加了一把锁,手里拿了钥匙),其他线程就都不能访问这个对象(没有打开该对象上锁的钥匙),直到A线程把这个“锁”撤掉。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
2、信号量(Semaphore):有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
以停车场为例:假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Request(请求)和 Release(释放)。 当一个线程调用Request(请求)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于1或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
信号量还可以设置是否采用公平模式,如果采用公平模式,则线程将会按到达的顺序(FIFO)执行;如果是非公平模式,则后请求的有可能排在队列的头部。构造方法是:Semaphore(int permits, boolean fair)。使用方法如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class Test
public static void main(String[] args)
// 线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 10; index++)
final int NO = index;
Runnable run = new Runnable()
public void run()
try
// 获取许可
semp.acquire();
System.out.println("第" + NO+"位进入");
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
//availablePermits()指的是当前信号灯库中有多少个可以被使用
System.out.println("可用信号量为:" + semp.availablePermits());
catch (InterruptedException e)
e.printStackTrace();
;
pool.execute(run);
// 退出线程池
pool.shutdown();
输出:
第0位进入
第2位进入
第1位进入
第3位进入
第4位进入
第5位进入
可用信号量为:0
可用信号量为:1
第6位进入
可用信号量为:1
第7位进入
可用信号量为:1
第8位进入
可用信号量为:1
第9位进入
可用信号量为:1
可用信号量为:2
可用信号量为:3
可用信号量为:4
可用信号量为:5
3、同步的四种机制:
1>使用synchronized关键字修饰方法或块
当程序运行到非静态的synchronized方法或块上时,自动获得当前实例对象的锁,直到退出synchronized方法或块才释放锁。
如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上线程进入该对象的一种池中(锁池队列),必须在那里等待,直到其锁被释放,该线程才能再次变为可运行或运行状态。
wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,这些线程等待来自该对象的信号(通知)。对象调用wait()方法获得这个等待列表。至此该对象不再执行任何该等待队列中线程中的动作,直到调用对象的notify()方法。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。使用wait()、notify()、notifyAll()方法时必须从同步块内调用这些方法:
1>public final void wait()
使已获取对象锁的当前线程释放对象锁,重新进入等待队列而成为等待状态;
2>public final void notify()
当前线程放弃此对象上的锁定后,才能继续执行被唤醒在此对象上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的;
3>public final void notifyAll()
当前线程放弃此对象上的锁定后,才能继续执行被唤醒在此对象上等待的所有线程。被唤醒的所有线程将以常规方式在锁池队列中进行竞争。
package test2;
class Bean
class Thread1 extends Thread
private String name;
private Bean bean;
public Thread1(String name,Bean bean)
this.name=name;
this.bean=bean;
public void run()
this.setName(name);
synchronized (bean)
try
System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"调用bean的wait()方法");
Thread.sleep(3000);
bean.wait();
System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
class Thread2 extends Thread
private String name;
private Bean bean;
public Thread2(String name,Bean bean)
this.name=name;
this.bean=bean;
public void run()
this.setName(name);
synchronized (bean)
try
System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"调用bean的notify()方法");
Thread.sleep(3000);
bean.notify();
System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
class Thread3 extends Thread
private String name;
private Bean bean;
public Thread3(String name,Bean bean)
this.name=name;
this.bean=bean;
public void run()
this.setName(name);
synchronized (bean)
try
System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"调用bean的notifyAll()方法");
Thread.sleep(3000);
bean.notifyAll();
System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
public class Test5
public static void main(String[] args)
Bean bean = new Bean();
Thread1 thread1 = new Thread1("Thread1",bean);
Thread2 thread2 = new Thread2("Thread2",bean);
Thread3 thread3 = new Thread3("Thread3",bean);
thread1.start();
thread2.start();
thread3.start();
结果:
线程Thread1获得bean的锁
线程Thread1调用bean的wait()方法
线程Thread3获得bean的锁
线程Thread3调用bean的notifyAll()方法
线程Thread3即将死亡
线程Thread2获得bean的锁
线程Thread2调用bean的notify()方法
线程Thread2即将死亡
线程Thread1即将死亡
可以看出:当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时或notifyAll()时,并不意味着当前线程会放弃其锁。因此调用notify()并不意味着对象锁变得可用。
注意:①synchronized只能修饰方法或块;
②不必同步类中所有的方法,类可以同时拥有同步和非同步方法;
③线程睡眠时,它所持的任何锁都不会释放;
④线程可以获得多个锁。比如在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁;
⑤同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块;
⑥如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
⑦要明确在哪个对象上同步。在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要指定获取哪个对象的锁。比如:
public synchronized void setNum(int tmp)
num = num-tmp;
和
public void setNum(int tmp)
synchronized(this)
num = num-tmp;
效果完全一样。
⑧同步静态方法,需要一个用于整个类对象的锁:
public static synchronized void setNum(int tmp)
num = num-tmp;
或
public static void setNum(int tmp)
synchronized(Bean.class)
num = num-tmp;
⑨静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
⑩当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。即使是线程安全类,也应该特别小心,因为操作的线程之间仍然不一定安全。
另外:在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep方法,因为synchronized程序块占着对象锁,你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率,也不合逻辑。
同样,在同步程序块内调用yeild方法让出CPU资源也没有意义,因为你占用着锁,其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。
2>使用volatile关键字修饰变量
volatile在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
为什么变量需要volatile来修饰呢?比如做一个i++操作,计算机内部做了三次处理:读取-修改-写入。同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题:如果这些步骤只执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。通过这个设想,就不难理解volatile关键字了。volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。
3>使用Lock接口
①java.util.concurrent.locks.Lock接口:实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。 不区分读写,称为“普通锁”;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//信用卡账户,可随意透支
class MyCount
private String oid; //账号
private int cash; //账户余额
MyCount(String oid, int cash)
this.oid = oid;
this.cash = cash;
public String getOid()
return oid;
public void setOid(String oid)
this.oid = oid;
public int getCash()
return cash;
public void setCash(int cash)
this.cash = cash;
public String toString()
return "帐户信息:[帐号:"+oid+",余额:"+cash+"]";
//信用卡的用户
class User implements Runnable
private String name; //用户名
private MyCount myCount; //所要操作的账户
private int iocash; //操作的金额,当然有正负之分了
private Lock myLock; //执行操作所需的锁对象
User(String name, MyCount myCount, int iocash, Lock myLock)
this.name = name;
this.myCount = myCount;
this.iocash = iocash;
this.myLock = myLock;
public void run()
//获取锁
myLock.lock();
//执行现金业务
System.out.println(myCount +","+ name + "正在操作账户,金额为" + iocash);
myCount.setCash(myCount.getCash() + iocash);
System.out.println(name + "操作账户成功,当前金额为" + myCount.getCash());
//释放锁,否则别的线程没有机会执行了
myLock.unlock();
public class Test
public static void main(String[] args)
//创建并发访问的账户
MyCount myCount = new MyCount("0000000000000000", 10000);
//创建一个锁对象
Lock lock = new ReentrantLock();
//创建一个线程池
ExecutorService pool = Executors.newCachedThreadPool();
//创建一些并发访问用户,一个信用卡,存的存,取的取
User u1 = new User("张三", myCount, -4000, lock);
User u2 = new User("张三他爹", myCount, 6000, lock);
User u3 = new User("张三他弟", myCount, -8000, lock);
User u4 = new User("张三", myCount, 800, lock);
//在线程池中执行各个用户的操作
pool.execute(u1);
pool.execute(u2);
pool.execute(u3);
pool.execute(u4);
//关闭线程池
pool.shutdown();
输出:
帐户信息:[帐号:0000000000000000,余额:10000],张三正在操作账户,金额为-4000
张三操作账户成功,当前金额为6000
帐户信息:[帐号:0000000000000000,余额:6000],张三正在操作账户,金额为800
张三操作账户成功,当前金额为6800
帐户信息:[帐号:0000000000000000,余额:6800],张三他弟正在操作账户,金额为-8000
张三他弟操作账户成功,当前金额为-1200
帐户信息:[帐号:0000000000000000,余额:-1200],张三他爹正在操作账户,金额为6000
张三他爹操作账户成功,当前金额为4800
②java.util.concurrent.locks.ReentrantReadWriteLock类:为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,称为“读写锁”。构造方法中可设置“公平模式”与“非公平模式”。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//信用卡账户,可随意透支
class MyCount
private String oid; //账号
private int cash; //账户余额
MyCount(String oid, int cash)
this.oid = oid;
this.cash = cash;
public String getOid()
return oid;
public void setOid(String oid)
this.oid = oid;
public int getCash()
return cash;
public void setCash(int cash)
this.cash = cash;
public String toString()
return "帐户信息:[帐号:"+oid+",余额:"+cash+"]";
//信用卡的用户
class User implements Runnable
private String name; //用户名
private MyCount myCount; //所要操作的账户
private int iocash; //操作的金额,当然有正负之分了
private ReadWriteLock myLock; //执行操作所需的锁对象
private boolean isQuery; //是否是查询帐户
User(String name, MyCount myCount, int iocash, ReadWriteLock myLock, boolean isQuery)
this.name = name;
this.myCount = myCount;
this.iocash = iocash;
this.myLock = myLock;
this.isQuery = isQuery;
public void run()
if (isQuery)
//获取读锁
myLock.readLock().lock();
System.out.println(myCount +","+ name + "正在查询账户余额");
//释放读锁
myLock.readLock().unlock();
else
//获取写锁
myLock.writeLock().lock();
//执行现金业务
System.out.println(myCount +","+ name + "正在进行存取操作,存取金额为" + iocash);
myCount.setCash(myCount.getCash() + iocash);
System.out.println(name + "操作账户成功,当前金额为" + myCount.getCash());
//释放写锁
myLock.writeLock().unlock();
public class Test
public static void main(String[] args)
//创建并发访问的账户
MyCount myCount = new MyCount("0000000000000000", 10000);
//创建一个锁对象
ReadWriteLock lock = new ReentrantReadWriteLock(false);
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建一些并发访问用户,一个信用卡,存的存,取的取,好热闹啊
User u1 = new User("张三", myCount, -4000, lock, false);
User u2 = new User("张三他爹", myCount, 6000, lock, false);
User u3 = new User("张三他弟", myCount, -8000, lock, false);
User u4 = new User("张三", myCount, 800, lock, false);
User u5 = new User("张三他爹", myCount, 0, lock, true);
//在线程池中执行各个用户的操作
pool.execute(u1);
pool.execute(u2);
pool.execute(u3);
pool.execute(u4);
pool.execute(u5);
//关闭线程池
pool.shutdown();
输出:
帐户信息:[帐号:0000000000000000,余额:10000],张三正在进行存取操作,存取金额为-4000
张三操作账户成功,当前金额为6000
帐户信息:[帐号:0000000000000000,余额:6000],张三他爹正在进行存取操作,存取金额为6000
张三他爹操作账户成功,当前金额为12000
帐户信息:[帐号:0000000000000000,余额:12000],张三他弟正在进行存取操作,存取金额为-8000
张三他弟操作账户成功,当前金额为4000
帐户信息:[帐号:0000000000000000,余额:4000],张三正在进行存取操作,存取金额为800
张三操作账户成功,当前金额为4800
帐户信息:[帐号:0000000000000000,余额:4800],张三他爹正在查询账户余额
③java.util.concurrent.locks.Condition接口:条件变量将 Object 的方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的,这样,条件就和一个锁对象绑定起来了。因此Java中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。
条件变量常用方法
1)void await():使当前线程处于等待状态直到被唤醒。
2)void signal():唤醒一个等待线程,如果所有的线程都在等待此条件,则选择其中的一个唤醒。
3)void signalAll():唤醒所有等待线程,如果所有的线程都在等待此条件,则唤醒所有线程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//普通银行帐户,不可透支
class MyCount
private String oid; //账号
private int cash; //账户余额
private Lock lock = new ReentrantLock(); //账户锁
private Condition saveCondition = lock.newCondition(); //存款条件
private Condition drawCondition = lock.newCondition(); //取款条件
MyCount(String oid, int cash)
this.oid = oid;
this.cash = cash;
//存款
public void saving(int x, String name)
lock.lock(); //获取锁
if (x > 0)
cash += x; //存款
System.out.println(name + "存款" + x + ",存款之后余额为" + cash);
drawCondition.signalAll(); //唤醒所有等待线程。
lock.unlock(); //释放锁
//取款
public void drawing(int x, String name)
lock.lock(); //获取锁
try
if (cash - x < 0)
System.out.println(name + "取款" + x + ",但余额只有" + cash +",余额不足");
drawCondition.await(); //余额不足,阻塞取款操作
else
cash -= x; //取款
System.out.println(name + "取款" + x + ",取款之后余额为" + cash);
saveCondition.signalAll(); //唤醒所有存款操作
catch (InterruptedException e)
e.printStackTrace();
finally
lock.unlock(); //释放锁
//存款线程类
class SaveThread extends Thread
private String name; //操作人
private MyCount myCount; //账户
private int x; //存款金额
SaveThread(String name, MyCount myCount, int x)
this.name = name;
this.myCount = myCount;
this.x = x;
public void run()
myCount.saving(x, name);
//取款线程类
class DrawThread extends Thread
private String name; //操作人
private MyCount myCount; //账户
private int x; //存款金额
DrawThread(String name, MyCount myCount, int x)
this.name = name;
this.myCount = myCount;
this.x = x;
public void run()
myCount.drawing(x, name);
public class Test
public static void main(String[] args)
//创建并发访问的账户
MyCount myCount = new MyCount("0000000000000000", 10000);
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
Thread t1 = new DrawThread("张三", myCount, 9000);
Thread t2 = new SaveThread("李四", myCount, 3600);
Thread t3 = new DrawThread("王五", myCount, 8000);
Thread t4 = new SaveThread("老张", myCount, 600);
Thread t5 = new DrawThread("老牛", myCount, 2000);
Thread t6 = new DrawThread("胖子", myCount, 800);
//执行各个线程
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
//关闭线程池
pool.shutdown();
输出:
张三取款9000,取款之后余额为1000
王五取款8000,但余额只有1000,余额不足
李四存款3600,存款之后余额为4600
老张存款600,存款之后余额为5200
老牛取款2000,取款之后余额为3200
胖子取款800,取款之后余额为2400
4>使用原子量java.util.concurrent.atomic包中的变量类 以上是关于Java之多线程的主要内容,如果未能解决你的问题,请参考以下文章