Java学习笔记(二十四):多线程及其安全(二)
线程通信
Java中的线程通信可以通过一个生产者——商品——消费者模型来体现。
假设有一种商品,它由生产者生产,由消费者消耗。当商品有剩余时,生产者进入等待状态。而消费者则对商品进行消耗;
当商品被消耗完的时候,消费者进入等待状态,并且提醒正在等待的生产者进行生产。此时生产者被唤醒,进行商品的生产,生产完成后再次进入等待状态,并且提醒消费者商品被已经生产出来了,以此往复。
而在Java中,生产者和消费者就好比两个线程,他们通过商品进行通信。在Object类中,存在两个方法:wait()和notify()。
void wait ():在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
void wait (long timeout): 在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待。
void notify ():唤醒在此对象监视器上等待的单个线程。
void notifyAll ():唤醒在此对象监视器上等待的所有线程。
现以代码形式模拟生产者和消费者模型:
//首先定义商品——一个食物类
public class Food {
public boolean flag = false;
}
//生产者模型
public class Producer implements Runnable {
Food food;
public Producer(Food food) {
this.food = food;
}
@Override
public void run() {
while (true) {
synchronized (food) {
//如果还有食物,则进入等待
if (food.flag) {
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有食物,进行生产
System.out.println("食物出炉啦");
food.flag = true;
food.notify();
}
}
}
}
//消费者模型
public class Consumer implements Runnable{
Food food;
public Consumer(Food food) {
this.food = food;
}
@Override
public void run() {
while (true) {
synchronized (food) {
//没有食物了,进行休眠
if (!food.flag) {
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消耗食物
System.out.println("买光啦");
food.flag = false;
food.notify();
}
}
}
}
//主方法
public class MyTest {
public static void main(String[] args) {
Food food = new Food();
new Thread(new Producer(food)).start();
new Thread(new Consumer(food)).start();
}
}
注意wait() 方法和 sleep() 方法的区别:
- 共同点:都可以使线程处于阻塞状态;
- 不同点:
- sleep()必须设置时间量,wait() 方法可以设置时间量,也可以不设置时间量;
- wait() 一旦等待,就会释放锁。而sleep() 一旦休眠,不释放锁。
内存可见性问题 volatile
假设有如下代码:
class MyRunnable implements Runnable {
//volatile 可以内存可见性的问题,但是不能保证原子性的操作
boolean flag=false;
public boolean getFlag(){
return flag;
}
@Override
public void run() {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改flag的值为true
flag=true;
System.out.println("线程进来执行了"+flag);
}
}
public class MyTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
while (true){
if (myRunnable.getFlag()) {//true
System.out.println("进来执行了");
break;
}
}
}
}
代行运行的结果却只执行了支线线程中的输出语句。主线程中根本没有进入while循环中的if语句。
究其原因,还是因为上一篇博客讲的Java内存模型。
Java内存模型规定:
-
所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来),线程对变量的所有操作(读取,赋值)都必须在工作内存中进行;
-
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
在上述例子中,因为Java内存模型的缘故,支线线程在自己的工作内存中更新flag之后,并没有立即更新到主存中。导致主线程中的flag仍然为false,因此没有进入if语句中的程序。
由此涉及到 Java多线程中的一大特性:可见性。
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的。当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
对于可见性,Java提供了volatile关键字来保证可见性:
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,相较于 synchronized 是一种较为轻量级的同步策略。
值得注意的是:
- volatile修饰的变量对于多线程,不是一种互斥关系;
- 不能保证变量状态的“原子性操作” 。
CAS算法
CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。是一种无锁的非阻塞算法。
CAS 包含了 3 个操作数:
-
需要读写的内存值 V;
-
进行比较的值 A;
-
拟写入的新值 B;
当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。关于锁的具体分类,将放在另外一篇博客中详细解释。
该例使用原子变量实现上篇博客写过的卖票代码:
public class MyTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 10; i++) {
new Thread(myRunnable).start();
}
//i++ i-- 不是一个原子性的操作
//Java 已经给我们提供好了这个原子变量的类,他已经实现CSA算法
}
}
class MyRunnable implements Runnable{
// int piao=1;
//重新定义原子变量
AtomicInteger piao= new AtomicInteger(1);
@Override
public void run() {
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(piao++);
int andIncrement = piao.getAndIncrement(); //使用CAS中已经具有原子性的自增操作
System.out.println(Thread.currentThread().getName()+"==="+andIncrement);
}
}
}
线程的状态转换图及常见执行情况
一个线程具有以下几种状态和特性:
-
新建:线程被创建出来;
-
就绪:具有CPU的执行资格,但是不具有CPU的执行权;
-
运行:具有CPU的执行资格,也具有CPU的执行权;
-
阻塞:不具有CPU的执行资格,也不具有CPU的执行权;
-
死亡:不具有CPU的执行资格,也不具有CPU的执行权。
用图表示如下:
线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。
而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。在线程池中可以预先创建很多个线程,来读取任务队列中的任务来执行。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等到任务来了就不用临时再创建了,可以立刻开始服务。
当线程池的线程刚创建时,会让他们进入阻塞状态,等待某个任务的到来。 如果任务来了就会唤醒其中一个线程,让它拿到任务去执行即可。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法:
public static ExecutorService newCachedThreadPool():
根据任务的数量来创建线程对应的线程个数
public static ExecutorService newFixedThreadPool(int nThreads):
固定初始化几个线程
public static ExecutorService newSingleThreadExecutor():
初始化一个线程的线程池
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下添加任务的方法:
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。定时任务必须继承TimeTaks类。
在Java中,可以通过Timer和TimerTask类来实现定义调度的功能。
Timer:
public Timer()
public void schedule(TimerTask task, long delay):
延时delay ms执行任务。
public void schedule(TimerTask task,long delay,long period):延时delay ms执行任务,之后每间隔period ms之后重复执行。
public void schedule(TimerTask task, Date time):
在指定日期执行一次任务。
public void schedule(TimerTask task, Date firstTime, long period):从指定日期第一次执行任务开始,每隔period ms重读执行任务。
TimerTask:定时任务
public abstract void run()
public boolean cancel()
//具体实例
public class MyTest {
public static void main(String[] args) {
Timer timer = new Timer();
//让定时器,执行定时任务
MyTimerTask myTimerTask = new MyTimerTask(timer);
//等3秒之后执行任务
// timer.schedule(myTimerTask,3000);
//等3秒第一次执行任务,以后间隔1秒重复执行定时任务
timer.schedule(myTimerTask, 3000,1000);
//取消定时任务
// myTimerTask.cancel();
}
}
class MyTimerTask extends TimerTask{
private Timer timer;
public MyTimerTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
System.out.println("碰!爆炸了");
timer.cancel();
}
}
设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编写、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性以及代码的结构更加清晰。
设计模式分类如下:
- 创建型模式(创建对象的):
- 单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式;
- 行为型模式(对象的功能):
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
- 结构型模式(对象的组成):
- 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
单例模式
保证一个类的对象在内存中只有一个。
单例模式有两种写法:
- 懒汉式:把该类的构造私有,不让外部创建其对象。提供静态方法返回该类的一个实例,供外界使用。
public synchronized class Student{
//1.私有构造
private Student(){}
private static Student student = null;
//2.提供静态方法返回一个类的对象
public static Student getStudent(){
if(student == null){
student = new Student();
}
return student;
}
}
- 饿汉式:把该类的构造私有,不让外部创建其对象。类一加载就会创建该类对象。
public synchronized class Student{
//1.私有构造
private Student(){}
private static Student student = new Student;
//2.提供静态方法返回一个类的对象
public static Student getStudent(){
return student;
}
}
在实际开发中一般使用饿汉式。比如Runtime类,设计时就采用了饿汉式。
每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前的Runtime。应用程序不能创建自己的 Runtime 类实例。
Runtime类中有exec方法用以执行一些DOS命令。
public Process exec(String command) //执行Dos命令