进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程: 线程是进程中的一个执行单元,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
通过继承Thread类来创建并启动多线程的步骤如下:
public class threadTest extends Thread{
//线程执行体
@Override
public void run() {
for(int i = 1;i<=20;i++){
System.out.println("在学习"+i);
}
}
public static void main(String[] args) {
new threadTest().start();
for(int i = 1;i<=20;i++){
System.out.println("在听歌"+i);
}
}
}
采用java.lang.Runnable 是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
public class runnableTest implements Runnable {
@Override
public void run() {
for(int i = 1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"在学习"+i);
}
}
public static void main(String[] args) {
runnableTest rt = new runnableTest();
new Thread(rt,"张三").start();
new Thread(rt,"李四").start();
for(int i = 1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"在听歌"+i);
}
}
}
前面实现多线程的两种方式有一种很明显的缺点就是没有返回值
实现多线程的第三种方式: 实现Callable接口,重写call方法
public class CallableTest implements Callable {
/**
* 实现Callable接口必须重写call方法
*/
@Override
public Object call() throws Exception {
String [] str= {
"apple","pear","banana","orange","grape"};
int i=(int)(Math.random()*5);
return str[i];
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建任务
CallableTest ct = new CallableTest();
/**FutureTask同时实现了Runnable,Future接口。
* 它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
*/
//交付任务管理
//可以看成FutureTask实现了runnable接口
FutureTask<String> futureTask = new FutureTask<>(ct);
Thread t=new Thread(futureTask);
t.start();
System.out.println("获取结果:"+futureTask.get());
System.out.println("任务是否完成:"+futureTask.isDone());
}
}
以第一个程序为例:
程序启动运行main方法时,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用Thread子类对象的start方法时,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过上面这张图我们可以很清晰的看到多线程的执行流程,但是为什么可以完成并发执行呢?多线程执行时,到底在内存中是如何运行的呢?
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间
public class innerClassLambdaTest{
//静态内部类
/*static class test implements Runnable{
@Override
public void run() {
for(int i = 1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"在学习"+i);
}
}
}*/
public static void main(String[] args) {
//局部内部类
/*
class test implements Runnable{
@Override
public void run() {
for(int i = 1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"在学习"+i);
}
}
}
new Thread(new test()).start();
for(int i = 1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"在听歌"+i);
}*/
//匿名内部类
/*new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1;i<=20;i++){
System.out.println("在学习"+i);
}
}
}).start();*/
//jdk1.8新特性 lambda表达式 只需要关注线程体,往往适用于单个简单线程
new Thread(()->{
for(int i = 1;i<=20;i++){
System.out.println("在学习"+i);
}
}
).start();
}
}
4.2.6 Thread和Runnable的区别
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。在Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。一般建议使用Executors工程类来创建线程池对象。
几种常见的线程池:
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(new Runnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
System.out.println("延迟1秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
使用线程池中线程对象的步骤:
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)使用线程池对象的方法如下:
public Future> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
/**
* 使用线程池的方式创建线程
*/
public class ThreadPool implements Runnable {
@Override
public void run() {
System.out.println("申请使用小黄车 ");
try {
//模拟出票操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("成功使用到小黄车"+Thread.currentThread().getName());
System.out.println("将小黄车归还");
}
}
class ThreadPoolTest{
public static void main(String[] args) {
//创建一个线程池,里面包含的线程对象最大为2个
ExecutorService executorService = Executors.newFixedThreadPool(2);
ThreadPool th = new ThreadPool();
//从线程池中获取线程并执行
/**
* submit()方法执行完后,程序并不会终止,因为线程池控制了线程的关闭
* submit()方法执行完后就直接将线程归还给了线程池
*/
executorService.submit(th);
executorService.submit(th);
//将线程池关闭
//executorService.shutdown();
}
}
问题引入:模拟卖票
public class SellTickets implements Runnable{
private int tickets = 100;
@Override
public void run() {
while(true){
if(tickets>0){
//使用睡眠时间模拟一下出票操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前对象的名称
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖"+(tickets--)+"号票");
}
}
}
}
//测试类
public class SellTicketsTest {
public static void main(String[] args) {
new Thread(new SellTickets(),"窗口1").start();
new Thread(new SellTickets(),"窗口2").start();
new Thread(new SellTickets(),"窗口3").start();
}
}
运行结果部分截图:
显而易见:相同号码的票被重复售卖,这是绝对不允许出现的情况 。这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写
操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,
否则的话就可能影响线程安全。
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了**同步机制(synchronized)**来解决。
解决思想:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等(BLOCKED)。
参考示例:
/**
* 同步代码块实现线程同步问题
*/
public class SyncCodeBlock implements Runnable{
private int tickets = 10;
/**
* 创建锁对象
*/
Object lock = new Object();
@Override
public void run() {
while (true){
synchronized (lock){
if(tickets>0){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* 测试类
*/
class SyncCodeBlockTest{
public static void main(String[] args) {
SyncCodeBlock syn = new SyncCodeBlock();
Thread t1 = new Thread(syn,"窗口1");
Thread t2 = new Thread(syn,"窗口2");
Thread t3 = new Thread(syn,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外
等着。
格式:
public synchronized void method(){
需要同步操作的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
参考示例:
/**
* 同步方法实现同步操作
*/
public class SyncCodeMethod implements Runnable {
private int tickets = 10;
@Override
public void run() {
//调用同步方法
this.sellTickets();
}
/**
* 需要进行同步操作的代码
*/
public synchronized void sellTickets(){
while (true){
if(tickets>0){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 测试类
*/
class SyncCodeMethodTest{
public static void main(String[] args) {
SyncCodeMethod syn = new SyncCodeMethod();
new Thread(syn,"窗口1").start();
new Thread(syn,"窗口2").start();
new Thread(syn,"窗口3").start();
}
}
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。Lock锁也称同步锁,同步锁的加锁和释放锁的方法
返回值 | 方法 | 含义 |
---|---|---|
void | lock() | 加同步锁 |
void | unlock() | 释放同步锁 |
参考示例:
/**
* 同步锁实现同步操作
*/
public class SyncLock implements Runnable {
private int tickets = 10;
/**
*创建锁对象
*/
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//加锁
lock.lock();
if(tickets>0){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
}
}
}
class SyncLockTest{
public static void main(String[] args) {
SyncLock sync = new SyncLock();
new Thread(sync,"窗口1").start();
new Thread(sync,"窗口2").start();
new Thread(sync,"窗口3").start();
}
}
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,
有以下几种状态
线程状态 | 描述 |
---|---|
NEW(新建状态) | 线程刚被创建,但是并未启动。还没调用start方法 |
RUNNABLE(就绪状态) | 调用start方法,在JVM中运行的状态 |
RUNNING(运行状态) | 获得了CPU,开始执行run()方法的线程执行体 |
BLOCKED(阻塞状态) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态 |
WAITING(无限等待状态) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
TIMED_WAITING(休眠状态) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait |
TERMINATED(死亡状态) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
线程状态转换图:
以卖票操作为例:
@Override
public void run() {
while (true){
synchronized (lock){
if(tickets>0){
try {
Thread.sleep(1000); //线程睡眠1秒
System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
注意事项:
public class WaitAndNotify {
public static void main(String[] args) {
Object lock = new Object();
//模拟生产者消费者
//创建一个消费者线程
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("消费者要购买东西A!");
synchronized (lock){
try {
//等待生产者生产
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者已经生产好A");
System.out.println("-------------------");
}
}
}).start();
//创建一个生产者线程
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("生产者正在生产A");
synchronized (lock){
//生产A需要5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒消费者,告知产品已经做好
lock.notify();
}
}
}
}).start();
}
}
由上可知:一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
调用wait和notify方法需要注意的细节