一、进程、线程、多线程概念
进程:正在执行的程序称作一个进程。进程负责了内存空间的划分。它是系统进行资源分配和调度的一个独立单位。
线程:线程在一个进程中负责了代码执行。
多线程:在一个进程中有多个线程同时在执行不同的任务。
注意:1.运行任何一个java程序,jvm在运行的时候都会创建一个主线程执行main方法中所有代码。
2.一个java程序中至少有两个线程,一个是主线程负责main方法代码执行;一个是垃圾回收器线程,负责了回收垃圾。
多线程的优缺点:
(1)多线程的弊端
增加了cpu负担
降低了一个进程中线程的执行效率
引发了线程安全问题
出现了死锁现象
(2)多线程的好处
多线程最大好处在于可以同时并发执行多个任务;
多线程可以最大限度地减低CPU的闲置时间,从而提高CPU的利用率。
二、创建两种线程的方法
1、继承Thread类
声明形式:继承Thread类,重写run方法
class MyThread extends Thread {
//把自定义线程的任务代码写在run方法中
public void run( ) {
/* 覆盖该方法*/
}
}
调用线程:
MyThread thread = new MyThread();
thread.start();
注意:
(1)一个线程一旦开启,那么线程就会执行run方法中的代码,run方法千万不能直接调用,如果直接调用就相当于调用了一个普通方法而已,并没有开启线程。
(2)启动线程(调用start方法),线程将等待调度(到底什么时候被调度,不一定要看当前的操作系统分配资源的情况);调度后,自动调用其run方法,run方法是等着被自动调用的。
案例:
public class MyPrinterThread extends Thread {
public MyPrinterThread(String name) {
super(name);
}
//重写run()方法,run方法的方法体就是线程的执行体
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(this.getName()+"i"+i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 创建线程
MyPrinterThread t1 = new MyPrinterThread(“thread1”);
MyPrinterThread t2 = new MyPrinterThread(“thread2”);
/*
* 1、启动线程,线程将等待调度(到底什么时候被调度,不一定要看当前的操作系统分配资源的情况);
* 调度后,自动调用其run方法,run方法是等着被自动调用的 2、创建两个线程后发现,不同的线程对象可以调用同一个方法。
/
t1.start();
t2.start();
// 如果以前没有线程我肯定是一步一步来执行,先创建对象,执行t1.run在执行t2.run
System.out.println(“main方法的最后一条语句”);
/
* 不同的机器运行的结果是不一样的,每次都是随机的,再运行一次还是不一样, 不是像以前一样把这一段的代码执行,
* 再去执行下一段的代码。它们现在是完全并行的两个任务,可以看到它们在交替执行。
*/
}
}
2、实现runnable接口
声明形式:
class MyThread implements Runnable{
public void run( ) {
/* 实现该方法*/
}
}
调用方式:
MyThread r = new MyThread();
//创建一个线程作为外壳,将r包起来,
Thread thread = new Thread®;
thread.start();
注意:Runnable接口的存在主要是为了解决Java中不允许多继承的问题。
案例:
public class PrintRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// thread当前线程的名字
System.out.println(Thread.currentThread().getName()+“i=” + i);
}
}
}
public class TestRunnable {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
PrintRunnable pr2 = new PrintRunnable();
Thread t1 = new Thread(pr1,“线程1”);
Thread t2 = new Thread(pr2,“线程2”);
t1.start();
t2.start();
}
}
三、Thread类中的常用方法
1、线程状态
(1)创建:新创建了一个线程对象,此时它仅仅作为一个对象实例存在, JVM没有为其分配CPU时间片和其他线程运行资源。
(2)可运行(就绪状态):线程对象创建后,其他线程调用了该对象的start()方法。这时,线程已经得到除CPU时间之外的其它系统资源,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会。
(3)运行:就绪状态的线程获取了CPU执行权,执行程序代码。
(4)阻塞状态: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
(5)死亡:线程执行完它的任务时,由JVM收回线程占用的资源。
2、构造方法
3、常用方法:
4、常用的静态方法:
5、常用方法解析:
(1)优先级
线程优先级用1~10 表示,10的优先级最高,默认值是5或继承其父线程优先级;优先级高的,获取的可能性则高。
案例:t1.setPriority(1);t2.setPriority(10);
(2)sleep方法:
使线程停止运行一段时间,此时将处于阻塞状态。阻塞的时间由指定的毫秒数决定
(3)yield()方法:
出让时间片,但不会阻塞线程,转入可运行状态。
(4)sleep()和yield()对比
(5)join()方法:
阻塞当前线程,强行介入执行。
(6)setDaemon方法:
1.要将某个线程设置为后台线程的话,必须在它启动之前调用setDeamon方法;
2.我们自己创建的线程,叫前台线程也叫用户线程。后台线程创建的都叫后台线程
3.前台线程执行结束,整个进程就结束
四、线程同步
1、线程安全问题分析:
需求:模拟三个窗口同时在售50张票
问题1:为什么50张票被卖出了150次?
出现原因:因为num是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,三个线程对象就会有三份。
解决方案:把num票数共享出来给三个对象使用。使用static修饰。
问题2:刚刚的问题是解决了,但是为什么会出现两个窗口卖同一张票呢?–出现了线程安全问题
解决方案:一个时间只能由一个线程操作内容—线程同步机制
2、线程安全出现的根本原因
(1)存在两个或者两个以上的线程对象,而且线程之间共享着一个资源
(2)有多个语句操作了共享资源
3、线程同步的概念
(1)多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入线程“同步”机制,即各线程间要有先来后到;
(2)同步就是排队:几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作;
(3)确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
4、线程同步机制方式一:同步代码块
语法:
同步代码块要注意的事项:
1、任意一个对象都可以作为锁对象
2、在同步代码块中调用sleep方法并不会释放锁对象的
3、只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率。
4、多线程操作的锁对象必须是唯一共享的,否则无效。
6、线程同步机制方式二:同步方法
语法:
同步方法要注意:
1、如果是一个非静态的同步方法的锁对象是this对象;如果是静态的同步方法的锁,对象是当前函数所属的类的字节码文件(Class对象)。
2、同步方法的锁对象是固定的,不能由你来指定。
推荐使用:同步代码块
原因:
1、同步代码块的锁对象可以由我们随意指定;同步方法的锁对象是固定的,不能指定。
2、同步代码块可以很方便的需要被同步的范围,同步方法必须是整个方法的所有代码都被同步了。
7、线程同步的优缺点
优点:解决了线程安全问题、对公共资源的使用有序化
缺点:性能下降;会带来死锁
五、死锁
1、概念:线程死锁指的两个线程互相持有对方依赖的共享资源,造成无限阻塞
2、死锁案例:电池和遥控器(张三拿电池,李四拿遥控器,谁也不肯放手)
注意:这种情况一定会发生吗?不一定,当张三快速的执行完毕时
3、死锁出现的根本原因:(1)存在两个或者两个以上的线程 (2)存在两个或者两个以上的共享资源
4、死锁解决方案:没有方案。只能尽量避免发生
六、线程通信
1、概念:线程通讯指的是多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用。(一个线程完成了自己的任务的时候,要通知另外一个线程去完成另外一个任务)
2、常用方法:
(1)wait(): 等待–如果线程执行了wait方法,那么该线程会进入等待的状态,等待状态下的线程必须要被其他线程调用notify方法才能唤醒。
(2)notify(): 唤醒–唤醒等待线程其中的一个。
(3)notifyAll() : 唤醒所有等待的线程。
3、wait与notify方法要注意的事项:
(1) wait方法与notify方法是属于Object对象 的。
(2) wait方法与notify方法必须要在同步代码块或者是同步方法中才能使用。
(3)wait方法与notify方法必需要由锁对象调用。
3、生产者和消费者案例:
七、线程生命周期
八、线程池
1、并发队列:分两种阻塞队列和非阻塞队列。而线程池底层是阻塞队列
(1)队列特点:先进先出
(2)入队:假设队列的容量的长度为10
非阻塞队列:当队列中满了的时候,放入第11条数据,那么此条数据丢失。
阻塞队列:当队列满了的时候,进行等待,什么时候队列中有出队的数据,那么第11个再放进去。
(3)出队:
非阻塞队列:如果现在队列中没有元素,取数据,得到的是null。
阻塞队列:等待,什么时候放进去,再取出来。
(4)非阻塞队列案例
(5)阻塞队列案例
2、线程池概念
我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。
3、为什么使用线程池?
在一个应用程序中,需要多次使用线程意味着多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。在Java中,内存资源是及其宝贵的,所以,提出了线程池的概念。
创建—》就绪状态–》运行–》死亡
3秒 1秒 2s
几个没线程没关系,但是高并发系统中
线程池:10线程–100个任务 (10个线程执行1-10,然后就接着执行10-20 。10线程相当于共享的资源,不用创建,使用完不用销毁,这样就节省了创建和销毁的时间)
eg:学校100台电脑,有第101个学生等待,有学生退电脑然后借电脑。
4、线程池底层实现原理
(1)原理图
(2)构造方法参数说明:
1.corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
2.maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
3.keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
4.unit:指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
5.workQueue:线程池中的任务队列.
常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
(3)常用方法:
1.int getCorePoolSize():返回核心线程数。
2. int getPoolSize():返回池中当前的线程数。
3.BlockingQueue getQueue():返回此执行程序使用的任务队列。
4.void shutdown():关闭线程池
5.void execute(Runnable command):执行任务
(5)案例:
5、线程池分类
1、newCachedThreadPool创建一个可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。 线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。
案例:
2、newFixedThreadPool创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。定长线程池的大小最好根据系统资源进行设置。
创建方式:
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
3、newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
创建方式:Executors.newSingleThreadExecutor() ;
4、newScheduleThreadPool创建一个定长线程池,支持定时及周期性任务执行
6、实现Callable接口–创建线程的第三种方式
注意:1.底层Callable的接口
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的,方法可以有返回值,并且可以抛出异常。但是Runnable不行。
2.Callable需要依赖FutureTask,用于接收运算结果。一个产生结果,一个拿到结果。FutureTask是Future接口的实现类。
7、线程池的方法
(1)execute(Runnable)以异步的方式执行它,这种方式没有办法获取执行Runnable之后的结果,如果你希望获取运行之后的返回值,就必须使用 接收Callable参数的execute() 方法.
(2)submit(Runnable)会返回一个Future 对象用于判断Runnable任务是否结束执行
(3)submit(Callable):Future对象中可以获取Callable的返回值
(4)inVokeAny():返回集合中某个Callable对象的结果,而且无法保证调用之后返回的结果是集合中的哪个Callable结果,只知道它是这些Callable中的一个。
九、信号量
信号量为多线程协作提供了更为强大的控制方法。信号量是对锁的扩展。锁一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
eg:车位是共享资源,每辆车好比是一个线程,看门人是信号量。
public class SemapDemo implements Runnable{
//信号量对象,只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
public static void main(String[] args) {
final SemapDemo semapDemo = new SemapDemo();
//固定数目线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//模拟20个客户端访问
for (int i = 1; i <= 20;i++) {
//执行
executorService.execute(semapDemo);
}
//退出线程池
executorService.shutdown();
}
public void run() {
try {
//获取许可
semp.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId()+",done!");
//访问完,释放,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后一直线程阻塞
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
十、线程并发控制方法
1、volatile
volatile:用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。
特点:
(1)volatile具有可见性、有序性,不具备原子性。
(2)volatile不会让线程阻塞,响应速度比synchronized高。
原子性:原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成没问题,如果只执行了一部分,你得撤销(即事务中的回滚)已经执行的部分。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序(指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度),但是却会影响到多线程并发执行的正确性。
volatile适用场景
适用于读多写少的场景。可用作状态标志。JDK中volatile应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile。
2、synchronized
3、信号量
4、ThreadLocal 线程局部变量:ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
5、ReentrantLock重入锁
1.重入锁分为公平锁和非公平锁;公平锁保证在锁等待队列中, 各线程是公平的, 因此不会存在插队情况, 对锁的获取总是先进先出。非公平锁没有这个保证, 后申请锁的线程可能会先获得锁.
2.方法
(1)lock() : 等待获得锁, 等待过程中不会响应中断异常, 等到执行unlock释放锁时, 再响应中断
(2)lockinterruptibly() : 与lock的唯一区别在于,等待过程中会响应中断,抛出中断异常
(3)tryLock() :tryLock尝试在指定时间内获取锁, 并在等待时间内可以响应中断异常.
获取成功返回true, 否则返回false.
注意:还有其它方法ReadWriteLock读写锁、condition对象等
十、定时任务
1、Timer是Java最早提供的一个任务调度器,它可以支持定时任务和重复任务的调度
2、参数TimerTask是一个抽象类,通过一个run()抽象方法来定义任务执行的内容。
3、案例
public class TimerScheduleTest {
public static void main(String[] args) {
Timer timer = new Timer();//创建对象
//安排指定的任务在指定的时间开始执行任务,并开始重复固定的工作
timer.schedule(new NormalTask(), addOneSecond(new Date()), 1000);
}
//当前时间加上5秒
public static Date addOneSecond(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, 5);
return calendar.getTime();
}
}
class NormalTask extends TimerTask {
@Override
public void run() {
System.out.println(“正常的任务”);// 定时器执行的任务
}
}