Java之多线程知识理解(浅谈)

  • 多线程之快速入门

 线程与进程的区别

简单理解:进程就是应用程序(每个正在系统上运行的程序都是一个进程),每个进程包括多个线程。而线程是一组指令的集合,可以在程序里独立执行,一般把线程理解为轻量级的进程。

具体化理解: 在一台电脑里面小明发现有多个APP,想要边用网易云听歌边用idea写代码还想边用迅雷下载学习资料,然后使用迅雷发现有以前的多个未完成的下载的任务于是开始多个下载。在该案例里面可以把多个不同的应用的APP理解为进程,把迅雷里面的多个的下载的任务理解为线程。

注意:使用多线程是可以提高效率的,但是使用大量的线程也会影响性能,因为在操作系统里面需要对CPU进行大量的切换并且线程需要更多的内存空间。进程是所有线程的集合,每一个线程是进程中的一条执行路径。

多线程的创建

一、使用继承Thread类,重写run方法(不建议使用,应该考虑面向接口的编程)--记住开启线程的方法是start()不是run()

class CreateThread extends Thread {
	// run方法中编写 多线程需要执行的代码
	publicvoid run() {
		for (inti = 0; i< 10; i++) {
			System.out.println("i:" + i);
		}
	}
}

二、使用实现Runnable接口,重写run方法

class CreateRunnable implements Runnable {

	@Override
	publicvoid run() {
		for (inti = 0; i< 10; i++) {
			System.out.println("i:" + i);
		}
	}

三、使用内部匿名类

 Thread thread = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i< 10; i++) {
					System.out.println("i:" + i);
				}
			}
		});

四、实现Callable接口 --注意使用该接口需要得到FutureTask 实现类的支持

class ThreadDemo implements Callable<Integer> {
 
    @Override
    public Integer call() throws Exception {
        int sum = 0;
 
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
 
        return sum;
    }

注意:实现callable接口和Runnable接口的区别:

  •   (1)Callable规定的方法是call(),而Runnable规定的方法是run(). 
  •   (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。  
  •   (3)call()方法可抛出异常,而run()方法是不能抛出异常的。 
  •   (4)运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。 
  •   它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。 
  •   通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。 
  • Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。

五、企业级线程池的使用(以newCachedThreadPool为例) -- 更多请往下

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			final int temp = i;
			newCachedThreadPool.execute(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100);
					} catch (Exception e) {
						// TODO: handle exception
					}
					System.out.println(Thread.currentThread().getName() + ",i:" + temp);

				}
			});
		}

守护线程与用户线程

用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止 --用户自定义的线程

守护线程当进程不存在或主线程停止,守护线程也会被停止。--主线程main、GC线程(当主线程挂了,就没必要在清理没用的对象,就会和主线程一起停止)

多线程的运行状态转换图

简单理解为:当new一个线程时为新建状态,当调用start方法返回时为就绪状态,当CPU调用run方法时处于运行状态,当处于阻塞状态时可能为调用sleep、调用I/O被阻塞的操作、等待锁资源、等待触发条件,当调用完run方法时为死亡状态(或者出现异常自动死亡)。

Join()方法和Yield()方法

join():让其他线程变为等待 --比如我要本来无序的两个线程变为有序的,先执行完某线程之后在执行该线程

yield():暂停当前正在执行的线程,并执行其他线程 --比如我要A让B执行是可以使用,但是在实际上应该是没有是什么实际的作用

多线程分批处理数据(类似于分页)

--提供一个实现线程分页的工具类,具体实现自己手动实践。

static public<T> List<List<T>> splitList(List<T> list, int pageSize) {
		int listSize = list.size();
		int page = (listSize + (pageSize - 1)) / pageSize;
		List<List<T>>listArray = new ArrayList<List<T>>();
		for (int i = 0; i<page; i++) {
			List<T>subList = new ArrayList<T>();
			for (int j = 0; j<listSize; j++) {
				int pageIndex = ((j + 1) + (pageSize - 1)) / pageSize;
				if (pageIndex == (i + 1)) {
					subList.add(list.get(j));
				}
				if ((j + 1) == ((j + 1) * pageSize)) {
					break;
				}
			}
			listArray.add(subList);
		}
		return listArray;
	}
}

  • 多线程之线程安全 

什么情况下会出现线程安全

当多个线程在进行对同一个资源进行操作时就会出现线程安全问题 --这里的资源一般指的是在类中的全局变量和静态变量等

出现这种情况下怎么处理

俗话说的加锁(这里使用同步synchronized的机制加锁或者使用Lock加锁)-- ps:这里的同步指的是当多个线程在操作同一个资源时保证每个线程不受到其他线程的干扰。在单线程中的说到的同步意思是代码从上到下依次的执行存在依赖关系,当上一个操作挂了随之之后全部挂了。

同步代码块和同步函数

代码块:使用synchronized包起来的部分叫做代码块  --使用的锁为自定义的资源锁,注意使用的时候要使用同一把锁

synchronized(资源锁){
 可能会发生线程冲突问题
}

 同步函数:使用synchronized修饰的函数  --使用的锁为this

public synchronized void get() {}

静态同步函数:额外使用static修饰的同步函数  --使用的锁为该函数字节码对象__XXX.class

public static synchronized void get() {}

多线程死锁: 简单记忆:当多个线程在抢占的同一个资源时因为使用的同步机制给资源加了不止一个锁的时候也就是嵌套锁的时候,若A线程得到了锁S1,B线程得到了锁S2,而资源只有俩个锁都有时可以进行操作,此时发生死锁。(也可以理解为当多个线程抢占同一个资源时发生资源不足使得多个线程均处于阻塞状态)。

Java内存模式(JMM):指的是在内存中存在这样一个模式,主内存(存放全局变量)、本地内存(存储本地线程的私有变量)

--注意不要和JVM的Java内存结构混淆,然后Java内存模式是抽象的、抽象的、抽象的概念(不存在,只是有相关的概念理论)

Volatile 关键字:使得变量在多个线程之间可见  --注意虽然使得变量可见但是不保证原子性

说明:理解以上的JMM模式会发现,举一个案例:如果在主内存里面有一个标识为flag=false,然后通过两个本地的线程去改写该变量的值,开启线程同时运行,当t1拿到全局变量时改为true赋值给本地内存然后未刷新时CPU切换到t2,t2拿到后改为false。若在主程序判断false时停止,会发现t1未停止(因为存在局部变量存取了为提交数据),这时就可以使用该关键字去强制该线程去读主内存的值


  • 多线程之间的通讯

 用图说明:使用input线程去修改共同资源,使用out线程去读取公共资源

问题:直接使用线程去操作公共资源会出现对资源的误读操作(例如当input刚写完username,out就开始读到了上一个sex)

解决:使用同步加锁synchronized,在线程input和out上分别加对象锁

wait()、notify、notifyAll()方法

-wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。

-这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

--如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。

-如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

-如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

注意:一定要在线程同步中使用,并且是同一个锁的资源

wait()和sleep()方法的区别

-对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

-sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

-在调用sleep()方法的过程中,线程不会释放对象锁

-而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

-获取对象锁进入运行状态。

Lock锁的机制(jdk1.5)

lock锁的使用:

Lock lock  = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
  lock.ublock();
}

说明:Lock锁和synchronized锁的机制的最大的区别是,Lock是手动加锁和解锁,synchronized是全自动的(简单记忆就是一个手动挡,一个自动挡。然后手动挡从性能上比自动挡要好。)Lock在上锁的时候会判断是否上锁(专业名词叫非阻塞地获取锁)若没有就上锁,若存在线程中断就内部抛出异常并且释放锁。如果线程在指定时间内还未得到锁则返回。

Condition的使用方法   ---类似于Object的wait和notify

Condition condition = lock.newCondition();
res. condition.await();  类似wait
res. Condition. Signal() 类似notify

停止锁的方式

    1.  使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

    2.  使用stop方法强行终止线程(这个方法不推荐使用,因为stopsuspendresume一样,也可能发生不可预料的结果)。

    3.  使用interrupt方法中断线程。

ThreadLoca接口  --使用Map集合实现 put("线程名",值);

使用该接口时应该回想到JMM(Java内存模式),在该接口hreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。


多线程之Java并发包

----------------------------各类型的线程安全之间的比对-----------------

Vector与ArrayList区别

Vector的内部和ArrList都是使用数组实现的,对元素进行存储的。但是前者是线程安全的,后者是线程不安全的,但是在存储的时候建议不要使用Vector来存储,效率极低。

HashMap和HashTable

这俩者的最大的区别就是一个是线程安全的,一个是线程不安全的。

从存储上看:HashMap在存储键值对的时候是允许NULL值但是HashTable是不允许的

从效率上看:肯定的使用非线程比使用线程安全的效率快多了

------------问题:如果要在线程安全的情况下使用HashMap怎么使用--------

1.synchronizedMap:可以将线程不安全的集合变为线程安全集合

2.ConcurrentMap:在1.5之前因为Hashtable的性能极低于是乎进行了加强,该接口可以看做是对Hashtable的加强,在该底层使用了分段的机制(端Segment)默认并发级别为16,也就是最高支持16个线程的并发修改操作。记住:在1.8以后该接口放弃了加锁机制(放弃了Segment)直接使用了CAS算法无锁机制进行优化

简单介绍下CAS:其实在该底层使用了CAS(V,E,N)的三个参数来实现无锁的机制,当某个线程进来以后对比V和E的值,如果相等表示未有线程进行操作则将V的值赋给N,若不等说明有线程进行修改就将V直接返回

CountDownLatch接口可以实现类似计数器的功能

需求:假如我要线程AB在主线程main之前执行完之后在执行main

方法:在线程AB的需要多线程执行程序里面加上方法countDownLatch.countDown() -实现对初始化参数的减一 ,在主线程的执行程序里面加上方法-countDownLatch.await(); -调用当前方法主线程阻塞  countDown结果为0, 阻塞变为运行状态

并发队列:

1.ConcurrentLinkedQueue:

是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能BlockingQueue.它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。ConcurrentLinkedQueue重要方法:
add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会

2.BlockingQueue:

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
在队列为空时,获取元素的线程会等待队列变为非空。
当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

贴上队列Queue的关系图:


线程池的使用(以ThreadPoolExecutor为例分析)

相关参数及其含义:

corePoolSize: 核心池的大小。 当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能创建多少个线程;
keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。
unit: 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

创建方式:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

线程池原理:


猜你喜欢

转载自blog.csdn.net/qq_41360177/article/details/88675684