java 多线程

多线程

多线程,可以理解为应用到CPU的一条执行通道,当然CPU能否执行,也就取决于CPU的调度方案了

Thread 类

继承 Thread 类是玩多线程的一种方式

设置线程类继承Thread 并重写方法run

在主线程中可以创建该线程对象,并执行start方法启动线程,这时候JVM会帮你调用run方法

注意,多次启动线程是非法的!

线程名

主线程的线程名为main,其他线程默认为Thread-x
x 为标识数值

获取线程名的方法为Thread 类的 getName()方法

在JVM开启主线程也是线程,也是Thread对象,Thread类中有个静态方法

static Thread currentThread()

用以返回当前正在运行的线程,通过这个线程对象来获取主线程的线程名字

setName 设置线程名称

Thread的构造器也可以设置线程名称

线程休眠

Thread.sleep(sec) 以毫秒为单位休眠

这个休眠会产生异常,这个异常一般是线程在特殊情况下被唤醒导致

Runnable接口

创建线程对象

实现Runnable接口也是执行多线程程序的一种方式

只需要重写一个方法run即可

public class SubRun implements Runnable {
    public void run() {
        // todo
    }
}

开启线程

在Thread构造方法构造器可以传递一个Runnable实现类的参数,创建线程对象,开启线程通道

同样调用线程对象的方法start启动线程即可

public static void main(String args[] ) {
    SubRun sr = new SubRun();
    Thread t = new Thread(sr);
    t.start();
}

实现类的好处

避免了继承的弊端,接口可以多实现

实现接口的方式更加符合面向对象对象的方式,继承Thread类,将线程对象和线程任务耦合一起,一旦创建线程对象,就有了线程任务,难以分开

实现类的好处就是分离再封装对象,达到线程对象和线程任务的解耦

实现接口的方式,还可以方便资源共享

使用匿名内部类

继承类重写的方式写法

new Thread() {
	public void run() {
		System.out.println("....");
	}
}.start();

实现接口重写方式写法:

new Thread(new Runnable() {
	@Override
	public void run() {
		System.out.println("....");
	}

}).start();

线程状态图

线程Thread类中有个嵌套类 Status 里边记录了很多的状态

  • NEW: 线程对象已经创建,new 出来后就处于这张状态

  • RUNNABLE 方法start() 会进入到此状态 运行状态

  • BLOCKED 受阻塞状态,执行start()后未必到RUNNABLE,可能到达此状态,比如受同步锁限制或CPU限制,这是程序无法控制的

即使是RUNNABLE状态,中间由于CPU资源抢夺,可能又会回到受阻塞状态,这也是程序无法控制的

  • TIMED_WAITTING 休眠状态,等待另一个进程执行,唤醒后未必能马上运行,即受阻塞

  • WAIT 调用wait()方法,永远等待另一个线程执行完成,等待另一个线程唤醒,同样唤醒后未必马上执行,即受阻塞

  • TERMINATED 死亡状态,线程已执行完毕

线程池

线程池其实就是一个能容纳多个线程的容器,其中的线程可以反复使用,避免了创建线程和线程死亡带来的资源消耗

这样的话,线程池里的线程状态就更多地在有用的循环中跑

原理


ArrayList<Thread> threads = ...;

threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());

Thread t = threads.remove(0);
t.start();
threads.add(t);

内置线程池 Executors

JDK 1.5 后java内置了线程池技术,直接使用即可

线程池都是通过线程池工厂创建,再调用线程池的方法获取的线程

java.util.concurrent.Executors

这个类中的方法都是静态方法

ExecutorsServices newFiexedThreadPool(int nThreads)

返回的是接口实现类,其实就是线程池对象

submit(Runnable r)

接口实现类对象,调用方法submit(Runnable r),提交线程执行任务

调用后,只要线程池存在,程序就不会停下来

shutdown()

销毁线程池,一般不用

实现callable

在使用Runnable方法重写run,有一个不好之处在于run没有返回值,而且也不能抛出异常

这时候实现callable就来解决这个问题

Callable 类的call方法原型,返回值是泛型,重写的子类异常可抛可不抛

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

创建Callable实现类

import java.util.concurrent.Callable;

public class SubCall implements Callable<String> {

	@Override
	public String call() throws Exception {
		return "哈哈哈";
	}

}

调用线程

  1. 使用工程类Executors静态方法newFiexedThreadPool创建线程池对象
  2. 得到的ExecutorsServices接口实现类,调用submit(Callable c)方法,该方法将返回Future的实现类
  3. 利用Future接口的方法get拿到返回值
ExecutorService es = Executors.newFixedThreadPool(2);
Future<String> f = es.submit(new SubCall());
try {
	String s = f.get();
	System.out.println(s);
} catch (InterruptedException e) {
	e.printStackTrace();
} catch (ExecutionException e) {
	e.printStackTrace();
}

线程共享安全问题

问题的来源

线程安全问题,通常是多个线程共同操作同一个数据

参考如下线程对象

public class SubRun implements Runnable {

	private int ticket = 100;

	@Override
	public void run() {
		while (true) {
			if (ticket > 0) {
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(ticket--);
			}
		}
	}

}

假设创建了三个线程,方法run将进栈3次,这3个方法使用的成员变量是同一个,都在堆内存中。

同步代码块

synchronized(任意对象) {
// 共享数据的操作
}

加上了同步操作后,运行速度就变慢了

同步原理

同步的依赖: 同步对象,同步锁,对象监视器

没有同步锁的线程不能执行,只能等

每个线程遇到同步代码块后,都会进行判断对象同步锁还是否存在,有则“拿走”,没有则不能执行,“拿走”锁的线程执行完后,会归还同步锁给下一个线程使用

这样线程每次判断锁,获取锁,释放锁,就导致线程安全速度变慢的原理了

同步方法

一般同步代码块放在一个方法中,这样可以在方法的声明上添加synchronized关键字,从而实现同步方法

这样写的好处在于代码的简洁分离,不需要显示声明额外的同步锁对象

public synchronized void sellTicket() {
	if (ticket > 0) {
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(ticket--);
	}
}

虽然没有显示声明,但还是有锁的,这个锁对象就是this本类对象引用

如果静态方法是同步的,这个同步锁就不是this了,应该是本类自己,即本类名.class

Lock 接口

同步代码块还有一个问题在于,如果某个线程运行到同步代码块中抛出了异常,那么后面的代码也将不会执行,也就是同步代码块将不再执行

这样引发的一个严重问题就是,锁永远不释放了!

因此JDK1.5后提供了一个Lock接口,给我们带来主动释放的操作

也就是Lock接口可以替换掉synchronized关键字

private int ticket = 10;

private Lock lock = new ReentrantLock();

@Override
public void run() {
	while (true) {
		lock.lock();
		if (ticket > 0) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
			System.out.println(ticket--);
		}
		lock.unlock();
	}
}

同步嵌套和死锁

在java中,同个线程对象的死锁发生是很难的,因为每个线程执行的顺序是一致的,即使在不同的线程对象中,由于要保护的对象数据也不一致,因此也不会用同一个同步锁,这也一定程度上避免线程死锁的问题

但是如果真要恶搞一个死锁,也是可以的:

来看一下这段程序

@Override
public void run() {
	while (true) {
		if (i % 2 == 0) {
			lockA.lock();
			System.out.println(Thread.currentThread().getName() + "锁上lockA");
			lockB.lock();
			System.out.println(Thread.currentThread().getName() + "锁上lockB");
			lockB.unlock();
			System.out.println(Thread.currentThread().getName() + "释放lockB");
			lockA.unlock();
			System.out.println(Thread.currentThread().getName() + "释放lockA");
		} else {
			lockB.lock();
			System.out.println(Thread.currentThread().getName() + "锁上lockB");
			lockA.lock();
			System.out.println(Thread.currentThread().getName() + "锁上lockA");
			lockA.unlock();
			System.out.println(Thread.currentThread().getName() + "释放lockA");
			lockB.unlock();
			System.out.println(Thread.currentThread().getName() + "释放lockB");
		}
		i++;
	}
}

通过不断的死循环,增加线程同时进行的概率,并控制一个公共变量,使得不同的线程执行不同的代码,让同一个线程对象也能执行不同顺序的程序

这样虽然每次输出结果都不确定,程序永远不会停止,不是因为死循环,控制台输出也只会死在一个地方不动:

Thread-0锁上lockA

Thread-0锁上lockB

Thread-0释放lockB

Thread-0释放lockA

Thread-1锁上lockA

Thread-0锁上lockB

这就是典型的死锁现象

当然死锁的案例一般不会是这样产生的,能产生的地方,后期会引入一个案例专门讲解

线程通信

之前的线程安全同步问题,虽然能解决多线程对同个资源的互斥操作,但还不能解决多个线程对同一资源的同步操作

比如对同个资源需要两个线程进行不同的操作,而这个操作是有时间同步顺序的,比如线程A进行操作A后线程B才能进行操作B

这样就会可能带来的问题是线程B抢占了线程A的资源进行操作B,而该资源还未进行操作A,显然这样线程就处理不当了!

这也就是经典的生产者和消费者问题的案例

显然,这个问题是同步锁无法解决的

为了解决这个问题,解决手段一般是增加线程通信机制!

线程等待

wait()

这是Object下的方法,每个类都可以直接调用

这是无限等待的机制,只要不被唤醒,就会一直等待下去

线程唤醒

notify()

这是Object下的方法,每个类都可以直接调用

唤醒线程

线程等待和唤醒案例

这个案例完成的是,一个线程负责对一个对象赋值操作,另一个线程对该对象进行取值操作

要求的是这两个线程同步进行,赋值完后取值,取值完后再赋值

大体思路就是设计两个线程,一个输入线程,一个输出线程

输入线程赋值完成后,执行方法wait()永远等待

输出线程输出完成后,注意要先唤醒notify输入线程,自己再wait永远等待

输入线程被唤醒后,即可重新赋值,赋值后注意先唤醒notify输出线程,自己再等待

但是这还是有个问题:

  1. 如何确保赋值过程中不被输出进程占用来输出
  2. 如何确保输出线程不先抢占资源来输出(如果这样的话,输出的结果就是未输入的空值了)

对于问题1,解决的方法就是上同步锁,因为问题1本质上就是多线程抢占同一资源的问题

可以看到,即使是资源同步问题,也跟资源互斥问题相关的

而对于问题2,就需要在资源上增加一个信号量的标记状态flag了

这个信号量标志可以自己定义,因此我们定义为

信号量为true说明赋值完成(需要取值)

信号量为false说明取值完成(需要赋值)

因此这么一来,输入输出线程都要根据信号量来决定是否能进行操作,并维护该信号量

对于输入线程,信号量为true,则需要等待,信号量为false,才能进行赋值操作,操作完成后唤醒输出线程,并修改标记

对于输出线程,信号量为false,则需等待,信号量为true,才能进行输出操作,操作完成后唤醒并修改标记

这里需要注意的问题是,怎么准确去唤醒线程,唤醒操作是根据监视器的,因此必须根据锁来唤醒,否则会抛出非法监视器异常

java.lang.IllegalMonitorStateException

根据上述思路

先创建资源类:

public class Resource {
	private String name;
	private int age;
	private boolean signal;
}

在进行输入线程的设计:

@Override
public void run() {
	while (true) {
		synchronized (r) {
			if (r.getSignal()) {
				try {
					r.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} else {
				if (i % 2 == 0) {
					r.setName("张三");
					r.setAge(16);
				} else {
					r.setName("李四");
					r.setAge(20);
				}
				i++;
				r.setSignal(true);
				r.notify();
			}
		}
	}
}

输出线程的设计

public void run() {
	while (true) {
		synchronized (r) {
			if (!r.getSignal()) {
				try {
					r.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} else {
				System.out.println(r);
				r.setSignal(false);
				r.notify();
			}
		}
	}
}

Condition 接口

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值