系列文章目录
线程部分知识属于java进阶的前奏部分,将会说到一些关于程序之间的运行机制,和多个线程之间和底层JVM以及硬盘之间的交互关系,实用性比较强。
前言
你要偷偷地学习然后拖垮他们所有人~~
以下是本篇文章正文内容,下面案例可供参考
一、基础知识网络图
二、“进程”与“线程”
1、什么是“进程”
概念:生活中我们使用电脑的时候会与许多的程序打交道,然而程序本来都是禁止的,只有当它被启动后获取了电脑的CPU资源的时候运作起来过后才能被称为进程。
举例:我们可以把电脑的CPU(中央处理器)当做是一个工厂,而这个工厂中的各个部门的车间就是程序,当一个车间被运作起来后,这个车间就是一个进程,然而在这个车间中负责不同工作的工人就是该进程的个个线程。
说明: 单核CPU在任何时间点上只能运行一个进程;宏观并行,微观串行。解释:我们平时使用电脑打开程序的时候(例如:QQ,微信,迅雷)等,看起来我们使用这些程序的时候它们是一起运行的,实则不然,它们在CPU分配资源的同时会获取一个“时间片”的东西,在这个时间片中它们才是真正处于被激活中(即使用状态),时间片的单位非常小,是以纳秒为单位的,所以在每一次使用其它程序的时候切换的速度非常地快,从而给我们一种错觉感觉这些程序都是同时处于运行的状态中,这也就是“宏观下并行,微观下处于串行(以时间片为主要限制单位)”的意思
2、什么是“线程”
概念:线程,也称轻量级进程(Light Weight Process)。程序中的一个顺序控制流程,同时也是CPU的基本调度单位,进程是由多个线程组成的,彼此之间完成不同的工作,交替执行,因此称为多线程(几乎所有的进程内部都是多线程)
举例:我们使用迅雷下载器的时候,迅雷软件就是一个进程,而其中的多个下载任务就是多个线程。我们平时写Java的时候Java虚拟机就是一个进程,当众默认包含了主线程(Main),可以通过代码创建多个独立线程,与Main并发执行。
3、进程与线程的区别
进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
一个程序运行后至少有一个线程
一个进程可以包含多个线程,但是至少需要一个线程
进程间不能共享数据段地址,但同进程的线程之间可以。
三、线程的组成
1、线程的基本组成部分
1、CPU时间片:操作系统(OS)会为每个线程分配执行时间
2、运行数据:
堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象
栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
线程的逻辑代码,就下来的两种创建方式中会有所展示
2、两种方式创建线程
方式一、通过创建线程类(继承Thread关系下完成创建)
//创建线程类
//需要继承Thread 并重写其中的 run()方法。
public class Mythread extend Thread{
public void run(){
//编写线程功能完后曾五十次的打印
for(int i=1;i<=50;i++){
System.out.println("MyThread:"+i);
}
}
}
//编写测试类
public class Test01{
public static void main(String [] args){
//创建线程的子类对象
Mythread t1=new Mythread();
t1.start();//调用start方法,不要直接调用run()方法,虽然结果一样,但并非线程的内容
}
}
方式二、通过实现Runnable接口并实现其中的run()方法
public class TestCreateThread{
public static void main(String [] args){
//创建线程任务类对象task
MythreadTask task=new MythreadTask();
Thread t2=new Thread(task);//将任务递给线程对象,注意Thread是java提供的一个类,可以直接进行实例化
t2.start();//启动线程
}
}
//创建线程任务类,并实现Runnable接口
class MythreadTask implements Runnable{
//重写其中的run()方法
public void run(){
for(int i=1;i<=50;i++){
System.out.println("MyRunnable"+i);
}
}
}
四、线程的状态
1、线程的生命周期
一个线程的生命周期一共需要经历四个大阶段:1、初始状态 2、就绪状态 3、运行状态4、中止状态
其中需要注意的就是 CPU中关于时间片的使用机制,它控制着线程的运行过程。
2、线程操作的常用方法
(1)休眠—sleep()
语法: public static void sleep(long millis)
注意:休眠的时间单位为毫秒,其间隔非常短
案例如下:
//任务需求,让线程休眠
/**
*知识点:让线程休眠
*需求:编写一个抽取学员的花名册,要求倒数三秒后输出被抽中的学生名字
*/
Random ran=ran.nextInt(names.length);
//名单如下
String [] names ={"赵信","盖伦","李青","嘉文","亚索","永恩","薇恩","卢锡安","赛娜"};
int index=ran.nextInt(names.length);
for(int i=3;i>=1;i--){
System.out.println(i);
Thread.sleep(1000);//使用休眠 sleep() 让线程暂停 1秒
}
System.out.println(names[index]);
(2)放弃(礼让)—yield()
语法:public static void yield()
讲解:当前县城主动放弃时间片,回到就绪状态,竞争下一次时间片。
案例如下:
/*
*知识点:线程礼让-yield()
*需求:创建两个线程A,B,分别各打印1-100的数字,其中B一个线程,每打印一次,就礼让一次,观察实验结果
*/
public class Test{
public static void main(String[] args){
A a = new A();
B b = new B();
a.start();
b.start();
}
}
class A extends Thread{
@Override
public void run(){
for(int i=1;i<=100;i++){
System.out.println("A:"+i);
}
}
}
class B extends Thread{
@Override
public void run(){
for(int i=1;i<=100;i++){
System.out.println("B:"+i);
Thread.yield();//礼让:让当前线程对象退出CPU,成为就绪状态,但是并不代表线程A就一定会抢先一步
}
}
}
(3)结合—join()
语法:public final void join()
讲解:允许其他县城加入到当前线程中(多用于主线程与子线程的结合中)
案例如下:
//测试结核方法---join()
public class Test01 {
public static void main(String[] args) throws InterruptedException {
/**
* 知识点:线程的合并 - join
*
* 需求:主线程和子线程各打印200次,从1开始每次增加1,
* 当主线程打印到10之后,让子线程先打印完再打印主线程
*/
MyThread t = new MyThread();
t.start();
for (int i = 1; i <= 100; i++) {
System.out.println("主线程:" + i);
if(i == 10){
t.join();
}
}
}
}
class Mythread extends Thread{
@Override
public void run(){
for(int i=1;i<=100;i++){
System.out.println("子线程"+i);
}
}
}
(4)线程中断—interrupt()
该方法 主要用于手动改变线程状态
案例如下:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
/**
* 知识点:线程中断
*
*/
MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
t.interrupt();//改变线程状态
}
}
class MyThread extends Thread {
@Override
public void run() {
//isInterrupted() - 获取当前线程状态(true-中断 false-存活)
while(!Thread.currentThread().isInterrupted()){
System.out.println("111");
System.out.println("222");
System.out.println("333");
System.out.println("444");
}
}
}
(5)守护线程
解释:守护线程一般是java提供的,例如JVM会在每次程序运行完毕后进行垃圾回收机制,而这个垃圾回收便是守护线程之一
案例如下:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
/**
* 知识点:后台线程/守护线程
*/
GuardThread guardThread = new GuardThread();
guardThread.setDaemon(true);//将此线程设置为后台线程
guardThread.start();
for (int i = 1; i <=5; i++) {
System.out.println("前台线程:" + i);
Thread.sleep(1000);
}
}
}
class GuardThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("守护线程默默守护着前台线程...");
try {
//父类run方法没有抛异常,子类重写后也不能抛异常
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
五、线程的安全
1、线程不安全的原因:
1、当多线程并发访问临界资源时,如果破坏原子操作,可能胡照成数据不一致(线程之间互相争夺资源造成)
2、临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
3、原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省
2、线程同步(synchronized)俗称“线程锁”
线程同步方法一:
/*语法:
synchronized(临界资源对象){//对临界资源对象枷锁
代码(原子操作)
}
*/
注意:
1、每个对象都有一个互斥锁标记,用来分配给线程的
2、只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
3、线程退出同步代码块时,回什邡相应的互斥锁标记
线程同步方法二
/**
synchronized 返回值类型 方法名称(形参列表){
//当前对象(this)加锁
//代码(原子操作)
}
*/
注意:
1、只有拥有对象互斥所标记的线程,才能进入到该对象加锁的同步方法中
2、线程退出同步方法时,回什邡相应的互斥锁标记
使用synchonized注意事项:
避免锁的嵌套使用,不然非常容易出现“死锁”情况,容易影响用户体验度
3、线程通信
(1)等待(wait)
public final void wait();
public final void wait(long timeout)
必须在对obj加锁的同步代码块中,在一个线程中,调用obj.wait()时,这个线程会释放其拥有的所有锁标记,同时此线程阻塞在线程的等待队列中,进入等待队列。
(2)唤醒(notify)
public final void notify()
public final void notifyAll();
必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个货全部线程,对自身没有任何影响
六、线程池(高级多线程部分)
(1)线程池的概述:
线程池出现原因:线程是一个宝贵的内存资源、单个线程约占1MB的空间,过多分配容易照成内存溢出,其次频繁的创建线程以及销毁线程会增加虚拟机的回收效率、资源开销,照成程序性能下降。
线程池:
1、线程容器,可设定线程分配的数量上线
2、将预先创建爱你的线程对象存入池中,并重用线程池中的线程对象
避免频繁的创建和销毁
(2)线程池的原理
原理:将任务提交给线程池,有线程池分配线程、运行该人物,并在当前任务结束后复用线程
(3)获取线程池
常用的线程池接口和类(java.util.concurrent)
Executor:线程池的顶级接口
ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码
Executors 工厂类:通过此类可以获得一个线程池
重点:通过 new FixedThreadPool(int 线程池数量)获取固定数量的线程池
通过newCachedThreadPool()获得动态数量的线程池,如不够则创建新的,没有上限
(4)常用的三大线程池(案例展示)
1、创建单个线程的线程池
ExecutorService pool= Executors.newSingleThreadExecutor();
风险:无界任务队列,可以无限添加任务,任务多起来就会崩溃
2、创建指定数量的线程的线程池
ExecutorService pool =Executors.newFixedThreadPool(3);
风险:无界任务队列,可以无限添加任务,任务多起来就会崩溃,容易造成超出内存控件
3、创建带有缓冲区的线程池
ExecutorService pool=Executors.newCachedThreadPool();
最大线程数:Integer.MAX_VALUE,-- 最大线程数量
风险:直接提交队列(同步队列):没有容量队列
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test01 {
public static void main(String[] args) {
/**
* 知识点:线程池
*/
//1、创建单个线程的线程池
//ExecutorService pool= Executors.newSingleThreadExecutor();
//2、创建指定线程个数的线程池,此处创建了三个线程的线程池
// ExecutorService pool =Executors.newFixedThreadPool(3);
//3、创建带有缓冲区的线程池
ExecutorService pool=Executors.newCachedThreadPool();
for (int i=1;i<=10;i++){
Task task=new Task(i);
//将任务提交给线程池
//pool/execute(task);//无返回值的提交任务的方法
pool.execute(task);
}
//关闭线程池
pool.shutdown();
}
}
class Task implements Runnable{
private int i;
public Task(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.class.getName()+"----》"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
七、线程安全的高级拓展
1、Callable接口
解释:JDK5加入,与Runnable接口类似,实现之后代表一个线程任务
Callable具有泛型返回值、可以声明异常
2、语法
public interface Callable<V>{
public V call() throws Exception;
}
案例如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test01 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
/**
* 1.计算任务,一个包含了2万个整数的数组,分拆了多个线程来进行并行计算,最后汇总出计算的结果。
*
* 需求:使用线程池去处理
*/
//创建数组
int[] is = new int[20000];
//初始化数组中的数据
for (int i = 0; i < is.length; i++) {
is[i] = i+1;
}
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
//提交任务
Future<Integer> f1 = pool.submit(new Task(is, 0, 5000));
Future<Integer> f2 = pool.submit(new Task(is, 5000, 10000));
Future<Integer> f3 = pool.submit(new Task(is, 10000, 15000));
Future<Integer> f4 = pool.submit(new Task(is, 15000, 20000));
//获取任务返回值并合并
System.out.println(f1.get() + f2.get() + f3.get() + f4.get());
//关闭线程池
pool.shutdown();
}
}
//带有返回值的任务类
class Task implements Callable<Integer>{
private int[] is;
private int startIndex;
private int endIndex;
public Task(int[] is, int startIndex, int endIndex) {
this.is = is;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = startIndex; i < endIndex; i++) {
sum += is[i];
}
return sum;
}
}
2、重入锁
ReentrantLock: Lock 接口的实现类,与synchronized一样具有互斥锁功能,甚至其使用度要高一些
案例如下:
public class MyList{
//创建重入锁对象
private Lock locker =new ReentrantLock();
private String[] strs={"赵信","盖伦","嘉文","李青","泰隆"};
private int count=2;//元素个数
//添加元素
public void add(String value){
//显示开启锁
locker.lock();
try{
strs[count]=value;
try{
Thread.sleep(1000);//主动休眠一秒
}catch(InterruptedException e){
}coubt ++;
}finally{
locker.unlock();//释放锁
}
}
}
总结
有关线程部分的内容比较多,难度也比较大,尤其是后面的高级线程部分,线程池的操作,需要花费很多时间来处理(拖更理由)。