线程(Thread)

目录

一、概念

1、进程与线程

2、并行与并发:

3、应用程序的单线程与多线程:        

 二、线程的状态(5种):生命周期

1、新建状态:new 

2、就绪状态:Runnable

3、运行状态:Running

4、阻塞状态:Blocked

5、死亡状态:Dead

三、线程的实现方式 -- 2种

1、继承Thread类:

2、实现Runnable接口:

3、extends Thread 与 implements Runnable的区别:

四、线程锁:常见2类

 1、概念:

2、悲观锁:synchronized互斥锁(有罪假设)

3、乐观锁:

五、通过线程池创建线程:

1、作用:

2、使用方式一:ExecutorService接口结合Executors

1)线程类:

2)测试类:

3)测试结果:

3、使用方式二:Callable/Future


看图先理解程序、进程、线程区别点。

1、一个程序,可以启用多个进程(多实例);

2、一个进程,可以启用多个线程(多线程)。

在任务管理器中,我们时常看到像谷歌浏览器,运行时,进程列表中往往不止一个,并且详细信息中看到,每一个进程,线程数都是多个的。

一、概念

1、进程与线程

        线程是什么?

        线程,英文名Thread,是操作系统能够进行运算调度最小单位

        它被包含在进程之中,是进程中的实际运作单位。一个进程,可以开启多个线程,多线程的概念,使得同一个进程,可以同时并发处理多个任务。

        每个进程都是具备自己独立的内存,同一进程下的所有线程共享一个进程的内存,而每个线程,也会具备自己独立的内存。

        使用线程技术,需要先有进程,进程的创建是OS(Operating system操作系统)创建的,OS一般是由C或C++语言进行实现。

2、并行与并发:

1)并行:每个程序可以拥有单独一个处理器进行处理或可以单独拥有一个CPU核心处理

        并行通常是指程序的同时运行。指代多个程序运行时,每个程序运行时可以被分配单独拥有一个CPU(多处理器情况),或者拥有单独一个CPU核心(单处理器多核情况),那么此时,就称这部分程序的状态为并行!

2)并发:抢占CPU资源

        指一个处理器(CPU)需要被多个应用程序抢占资源使用,例如,单核CPU情况下,同时运行了QQ、浏览器等程序,那么这3个程序就会抢占这个CPU的共享资源,才可以正常运行,否则会进入挂起、阻塞等状态。

        在线程中,并发指代同时访问一个进程内存资源,假如并发量很高,此时往往会通过多线程技术进行处理。

        

3、应用程序的单线程与多线程:        

        在java中,我们运行其一个项目,如果,你并未使用多线程技术,此时即为单线程状态,你的所有功能运行时,都是只有一个线程运行,产生功能并发量多时,就容易卡顿、宕机。

        举个简单例子:登录功能,大部分项目基本会有登录功能,用于校验,那么在单线程情况下,如果同时多个用户去同时触发登录功能,就会导致卡顿(双十一、双十二的平台抢购也时同样道理),一般情况下,微小型应用可能用户量不多,即使多个用户使用了,也看不出来,时正常的,但是如果并发高达百万级别了,依旧使用单线程的话,那么恭喜你,可以去维修服务器了。

 二、线程的状态(5种):生命周期

1、新建状态:new 

        当线程对象创建好,该线程进入新建状态:

Thread thread = new MyThread(); //MyThread是个人创建的自定义线程类

2、就绪状态:Runnable

        当调用线程对象的start()方法,此时线程进入就绪状态。需要注意的是,调用start()只是说明线程就绪,这个时候的线程,并未实际执行。

Thread thread = new MyThread();
thread.start(); //进入就绪状态

3、运行状态:Running

        当CPU开始调度处于就绪状态的线程时,即当前线程获得CPU时间片,线程会调用run()方法开始真正执行,进入运行状态。就绪状态是运行状态的唯一入口,如果线程需要运行,首先必须处于就绪状态种。

        注意:进入运行状态,并非是直接手动调用run()方法去进入,而是进入就绪状态后,CPU调取后会自动调用run(),run()方法是你写好需要操作的代码,调用时被自动执行。

4、阻塞状态:Blocked

        处于运行状态(Running)的线程,由于某种原因,暂时放弃对CPU的使用权,停止执行,此时该线程会进入阻塞状态,直到其进入就绪状态(Runnable),才有机会再次被CPU调用,才可以再次进入到运行状态。

        根据阻塞产生的原因不同,阻塞状态会分3种:

1)等待阻塞:运行状态种的线程执行wait()方法,使线程进入等待阻塞状态;

2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它就会进入同步阻塞状态;

3)其他阻塞:通过调用线程的sleep()、join()或者发出IO请求时,线程会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或超时或者IO处理完毕时,线程重新进入就绪状态。

5、死亡状态:Dead

        线程执行完毕,或因为异常退出了run()方法,该线程会结束生命周期,进入死亡状态。

三、线程的实现方式 -- 2种

1、继承Thread类:

1)新建一个自定义的线程类,继承Thread父类,并且重写其run()方法作为操作方法:

//自定义一个线程类:需要继承Thread父类,代表其是线程类
public class MyThread extends Thread{
	/**
	 * 写线程业务:比如输出当前线程的名称10次
	 * 多线程业务的编写,放在线程的run()方法中
	 * alt+/ :可以直接引导重写出run()方法
	 */
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i < 10; i++) {
			System.out.println(i+getName());
		}
	}
	
}

2)代码测试:

//该类用于测试Thread线程的使用
public class TestThread {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread mt = new MyThread();//线程的新建状态new
		
		//下面的单个方法调用都是单线程
//		mt.run();//注意,直接调用run()方法的话,并非是多线程
		mt.start();//调用父类里的strat()方法才是真正意义上的启动线程,jvm会自动调用run()方法
		
		//模拟多线程:需要启动最少2个线程,如果只启动一个线程,那个叫单线程程序
		MyThread mt1 = new MyThread();
		mt1.start();//从新建状态转为可运行状态Runnable,等待CPU调度
	}

	//
}

3)运行结果:

要点分析:

a、首先通过结果得知普通线程的一个特性:随机性,上面创建了2个线程通过CPU进行调度时,会随机进行调度,而不是按顺序固定的;

b、其次,真正实现多线程的启动方法,并非直接调用run()方法,而是通过启用start()方法实现。(这个问题可能面试官问的多)。如果直接手动调用run()方法,其实就是普通的去按顺序执行了方法,而非真正的启用多线程。下图是直接调用run()的运行结果:

2、实现Runnable接口:

1)自定义一个线程类,去实现Runnable接口,重写其run()方法 :

package cn.tedu.Thread;
//多线程的编程方式2 : implements Runnable,多线程类
public class MyRunnableThread implements Runnable{

	@Override
	public void run() {
		// 把业务放进run方法里面:打印10次线程名称
		for(int i = 0; i < 10; i++) {
			/**
			 * Runnable接口中没有getName方法,只有run方法,
			 * 如果想要获取当前正在执行业务的线程名称,
			 * 那么可以通过Thread.currentThread()获取当前正在执行的线程名称
			 */
			System.out.println("线程名称:"+Thread.currentThread().getName()+";次数:"+i);
		}
		
	}
	
}

2)代码测试:

package cn.tedu.Thread;
//这个类用来测试多线程的编程方式2 : implement Runnable
public class TestRunnable {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyRunnableThread mr = new MyRunnableThread();
		//如何启动线程:
		Thread thread = new Thread(mr,"Runnable线程");
		thread.start();
		
		//模拟多线程
		Thread thread2 = new Thread(mr,"第二个线程");
		thread2.start();
	}

}

3)运行结果:

结果分析:

        从结果而言,效果是跟继承了Thread类是差不多。

3、extends Thread 与 implements Runnable的区别:

1)Thread:

优点:编写简单,可以直接调用父类方法编写程序。

缺点:不能再继承其他父类。

2)Runnable:

优点:保留了一个父类的位置,多线程共享一个target对象,适合多个相同线程处理同一份资源,更符合面向对象的编程思路。

        什么是多线程共享一个target:

缺点:代码复杂度提升,执行线程的功能,必须使用Thread原生态类去调用方法。

四、线程锁:常见2类

 1、概念:

        线程锁,简单来说,是用于防止线程由于线程并发时,由于线程不同步,可能出现的安全问题。通过对线程涉及的某个部分代码进行上锁,实现了同步机制,即为线程锁的作用。

        例如,假设把上面的程序,修改为一个存取款的案例,那么其中会有一个账户金额的概念,如果直接按上面的例子去做,大家可能就会发现,线程1存款后,线程2取款,然后查询发现金额可能不对,这个就是脏数据了,这个例子大家可以自己去尝试。

        而常见的线程锁,通常分为两类,分别是悲观锁、乐观锁。

2、悲观锁:synchronized互斥锁(有罪假设)

1)示例:

使用关键字:synchronized

//线程类中直接new一个独立的成员对象,每个线程进来本来时只会访问同一个对象
	Object o = new Object();
	public void run() {
		// 同步锁的位置:太大不行,程序效率太低,太小也不行,无效。
		while(true) {
			//由于目前是对共享数据的操作出现安全问题,所以对展业代码进行加锁
			synchronized (o) {//同步锁,是指同一时间资源没人抢占,一个对象使用
				//同步锁里面的代码块称为同步代码块:
				if(tickets>0) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
					System.out.println(Thread.currentThread().getName()+tickets--);
				}else break;
			}
		
		}
	}

2)关键字可用范围:代码块、方法

        悲观锁对于并发间操作产生的线程安全问题,保持悲观状态,认为资源抢占总会发生,所以也称之为互斥锁,其实现的同步机制是最严格。

        如果想使用这个锁,只需要把关键字synchronized(如上例子)放置于认为有安全问题的代码块内容,或者整一个方法上都可以

        但是需要注意的是,如果场景是读多写少的情况下,这会导致效率非常慢,所以我们常用于写多读少的情况。并且,锁放置的位置,也尽可能不要太大,避免放置于一个大的方法上,也不要去随便放置于run()方法上。

3、乐观锁:

1)示例:

关键字:lock

rivate Account account = new Account();
 //买一把锁
 Lock lock = new ReentrantLock(); //Re-entrant-Lock  可重入锁
 @Override
 public void run() {
 //此处省略300句
 try{
//上锁
 lock.lock();
 //判断余额是否足够,够,取之;不够,不取之;
 if(account.getBalance()>=400){
 try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                method1();
 //取之
 account.withDraw(400);
 //输出信息
 System.out.println(Thread.currentThread().getName()+
 "取款成功,现在的余额是"+account.getBalance());
            }else{
    System.out.println("余额不足,"+Thread.currentThread().getName()
                 +"取款失败,现在的余额是"   +account.getBalance());
            }
        }finally {
 //解锁
 lock.unlock();
        }
 //此处省略100句
 }

2)乐观锁解析:

        乐观锁,顾名思义,对并发间的线程安全问题,持乐观状态。乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较 – 替换这2个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑

        常用乐观锁 – ReentrantReadWriteLock读写锁(无罪假设)。

        ReentrantReadWriteLock是排他锁;

        排他锁在同一时刻仅有一个线程可以访问。

        实际上独占锁是一种相对保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读写操作之间不存在数据竞争,如果“读/读”操作能够以共享锁的方式进行,那会进一步提升性能。因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)、Read(读)、Write(写)、Lock(锁),我们下面称它为读写锁。

        读写锁内部又分为读锁和写锁;

        读锁可以在没有写锁的时候被多个线程同时持有;

        写锁是独占的。

        读锁和写锁分离 从而提升程序性能,读写锁主要应用于读多写少的场景。

3)应用场景与方式:

读写锁由于读写分离的性质,常用于读多写少的程序场景,效率较高。

锁定写业务的方式:

lock.writeLock().lock();

五、通过线程池创建线程:

1、作用:

1)可以直接创建出足够多个线程,无需通过手动不断地new进行新建;

2)线程的新建、启用、关闭任务都交给了线程池进行处理,无需手动关注线程资源使用进展;

3)线程池中的线程是自动同步的, 无需考虑加线程锁的情况。

4)线程池建立的线程是可以复用的,不会导致资源使用时新建。

总的来说,线程池的出现,减少了大量的线程建立与运行时产生的代码以及安全问题,所以,一般多线程技术的使用,常常伴随线程池去操作。

2、使用方式一:ExecutorService接口结合Executors

        在这里,我们需要明确一下,线程的实现方式一般是指继承Thread类,或者实现Runnable接口2种方式,而线程池则是基于这2个实现线程技术方式的情况下,延申出来的。所以,线程池建立线程,一般称之为通过线程池可以如何建立线程(线程池使用方式),而非线程的实现方式(这个可能面试官会问,所以注明下)。

        那么综上,如果需要使用线程池,自然也是需要自定义自己的线程类(要么继承Thread,要么实现Runnable)。

        下面通过例子直接体验。

1)线程类:
package cn.tedu.threadPool;
//测试线程池方法建立线程 的 线程类对象
public class PoolThread implements Runnable{
	int ticket = 1000;
	@Override
	public void run() {
		//注意:使用了线程池的情况下,底层代码会自动进行线程同步,不需要手动加同步锁
//		synchronized (this) {
			while(true) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				if(ticket>0) {
					System.out.println("线程名称:"+Thread.currentThread().getName()+",执行任务:"+ticket--);
				}else break;
			}
		}
	
		
//	}

}

2)测试类:
package cn.tedu.threadPool;

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

//该类用于测试其他的线程创建方式 -- 线程池  ExecutorService接口以及Executors辅助类

public class TestExecutor {
	public static void main(String[] args) {
		//创建线程对象
		PoolThread target = new PoolThread();
		
		//把新建线程,启动线程,关闭线程的任务交给线程池处理,ExecutorService
		//Exectors类用于辅助创建线程池对象,newFixedThreadPool(n) -- 建立具有固定线程数n的线程池,允许同时存在n个线程
		ExecutorService pool = Executors.newFixedThreadPool(5); //建立4线程数的线程池
		
		for(int i = 0; i < 5; i++){
		
		//让线程池执行任务 -- 线程池.execute(target);
			pool.execute(target);
		}
	}
}

3)测试结果:

3、使用方式二:Callable/Future

        对于线程池建立多线程的使用方式,一般正常都会使用ExecutorService接口结合Executors调用方法,所以第二个方式大家了解下,看一下有这么个方式,实际开发中可能比较少用,真的有需要的,可以自行搜索研究下即可。

  • 33
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值