程序只有一个执行流程,所以这样的程序就是单线程程序。
假如一个程序有多条执行流程,那么,该程序就是多线程程序。
线程是依赖于进程而存在的
进程:就是正在运行的程序。
是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
单进程的计算机只能做一件事情,现在的计算机都是支持多进程的。可以在一个时间段内执行多个任务,并且可以提高CPU的使用率。
单CPU在某个时间点只能做一件事情,CPU在做程序间的高效切换让我们觉得是同时进行的。
线程:在同一个进程内又可以执行多个任务,而这每一个任务就可以看作是一个线程。是进程中的单个顺序控制流,是一条执行路径
线程是程序的执行单元,执行路径。是程序使用CPU的最基本的单位。
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
多线程的存在不是提高程序的执行速度,其实是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权,多个线程都在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。我们不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。
并行:逻辑上同时发生,指在某一个时间内同时运行多个程序
并发:物理上同时发生。指在某一个时间点同时运行多个程序
java程序的运行原理:由java命令启动了JVM,JVM启动就相当于启动了一个进程。接着由该进程创建了一个主线程去调用main方法
jvm虚拟机的启动是单线程的还是多线程的?
多线程的。因为垃圾回收线程也要先启动,否则就会出现内存溢出。
如何实现多线程的程序:
A:继承Thread类
步骤:1.自定义类继承Thread类
\2. 在自定义类内重写run()方法
\3. 创建该类的对象
\4. 启动线程
为什么要重写run()方法:不是类中的所有代码都需要被线程执行的。为了区分哪些代码能够被线程执行,java提供了Thread类中的run()方法用来包含那些被线程执行的代码
//一般来说,被线程执行的代码肯定是比较耗时的。所以用循环改进
run()和start()方法的区别:
run()仅仅是封装被线程执行的代码,直接调用是普通方法
start()首先启动了线程,然后由jvm去调用该线程的run()方法
如何获取和设置线程名称
public final String getName() 获取线程的名称
public final void setName(String name)
通过构造方法也可以给线程起名字,带参构造
如何获取main方法所在的线程名称呢?
public static Thread currentThread() 返回当前正在执行的线程对象
Thread.currentThread().getName() 这样就可以获取任意方法所在的线程名称
线程有两种调度模型:
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
如何设置和获取线程优先级
public final int getPriority() 返回线程对象的优先级 线程默认优先级是5
public final void setPriority(int newPriority) 更改线程的优先级 线程优先级的范围是1-10
线程优先级仅仅表示线程获取CPU时间片的几率高,但是要在次数比较多或者多次运行的时候才能看到比较好的效果
线程控制
线程休眠
public static void sleep(long millis)
线程加入
public final void join() 等待该线程终止
线程礼让
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程
让多个线程的执行更和谐,但是不能靠它保证一人一次
后台线程
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程
当正在运行的线程都是守护线程时,java虚拟机退出,该方法必须在启动线程前调用
中断线程
public final void stop() 已过时。具有不安全性,不建议使用
public void interrupt() 中断线程。把线程状态终止,并抛出一个InterruptedException
线程的生命周期:
新建:创建线程对象
就绪:有执行资格,没有执行权
运行:有执行资格,有执行权
阻塞:由于一些操作让线程处于该状态,没有执行资格,没有执行权。而另一些操作却可以把它激活,激活后处于就绪状态
死亡:线程对象变为垃圾,等待回收
B:实现Runnable接口
步骤:1.自定义类实现Runnable接口
\2. 重写run()方法
\3. 创建自定义类对象
\4. 创建Thread类对象,并把3中的对象作为构造参数传递
B方式的好处,实现接口方式的好处:
可以避免由于Java单继承带来的局限性。
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
电影院售票出现问题:
1.相同的票出现多次:CPU的一次操作必须是原子性的
2.还出现了负数的票:随机性和延迟导致的
判断是否有多线程问题的标准:
1.是否是多线程环境
2.是否有共享数据
3.是否有多条语句操作共享数据
解决线程安全问题实现:
1.同步代码块
格式:
synchronized(对象){需要同步的代码;}
同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。这个对象可以是任意对象。
同步的前提:多个线程,多个线程使用的是同一个锁对象
同步的好处:同步的出现解决了多线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
2.同步方法
就是把同步关键字加到方法上(一般在权限修饰符后面)
同步方法的锁对象是:this
静态方法的同步方法的锁对象:是当前类的字节码文件对象。(反射)
如果锁对象是this,就可以考虑使用同步方法。
否则能使用同步代码块的尽量使用同步代码块。
线程安全的类:
StringBuffer
Vector<> 即使要安全,也不用。可以用Collections工具类中的静态方法synchronizedXxx方法
Hashtable<,>
JDK5以后提供了一个新的锁对象Lock
Lock是一个接口
void lock() 获取锁
void unlock() 释放锁
ReentrantLock Lock的实现类。这个类同时也实现了序列化Serializable接口
可能加锁后的代码会出问题,导致无法释放锁。所以可以选择加try…finally语句
同步弊端:效率低;如果出现了同步嵌套,就容易产生死锁问题
死锁问题:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
*public* *class* DieLock *extends* Thread {
*private* *boolean* flag;
*public* *static* *void* main(String[] args) {
DieLock l1 = *new* DieLock(*true*);
DieLock l2 = *new* DieLock(*false*);
l1.start();
l2.start();
}
*public* DieLock(*boolean* flag) {
*this*.flag = flag;
}
@Override
*public* *void* run() {
*if* (flag) {
*synchronized* (MyLock.****objA****) {
System.****out****.println(“if的ogjA”);
*synchronized* (MyLock.****objB****) {
System.****out****.println(“if的ogjB”);
}
}
} *else* {
*synchronized* (MyLock.****objB****) {
System.****out****.println(“else的ogjB”);
*synchronized* (MyLock.****objA****) {
System.****out****.println(“else的ogjA”);
}
}
}
}
}
线程间通信问题:不同种类的线程间针对同一个资源的操作
通过设置线程(生产者)和获取线程(消费者)针对同一个对象进行操作
1.如何保证是同一个对象的操作:在外界把这个对象创建出来,然后通过构造方法传递给其他的类。
2.CPU的时间片在很短的时间内可以执行很多条代码会产生相同的数据,线程抢占的随机性也会导致安全问题:所以要加锁。不同种类的线程都要加锁,而且这加的锁必须是同一把才能保证安全。(传的对象要是同一个)
3.如果消费者先抢到执行权,就会先执行,但是会输出默认值,无意义。如果生产者先抢到先执行后再抢到会继续生产,不合常理。所以应该:生产者先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费数据。消费者先看是否有数据,有就消费,没有就等待,通知生产者生产数据。
所以java提供了一种机制:等待唤醒机制实现数据依次出现
Object类中提供了三个方法:
wait() 等待 会立即释放锁,当唤醒时从此处继续执行
notify() 唤醒单个线程 唤醒并不代表立即执行,还需要抢夺执行权
notifyAll() 唤醒所有线程
为什么这些方法不定义在Thread类中:因为这些方法的调用必须通过锁对象调用,而我们使用的同步块锁对象是任意锁对象。所以这些方法必须定义在Object类中
把同步代码块改进为同步方法实现,以后只需要调用方法即可
ThreadGroup 线程组:把多个线程组合到一起。它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制
线程类里面的方法:
public final ThreadGroup getThreadGroup()
线程组里面的方法:public final String getName()
默认情况下,所有的线程都属于主线程组main
也可以给线程设置分组:创建一个线程组;创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
Thread(ThreadGroup group, Runnable target, String name)
线程组的意义:通过组名称调用方法,可以设置此线程组的各种优先级,状态等等。
线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
JDK5新增了一个Executors工厂类来产生线程池
public static ExecutorService newCachedThreadPool() 创建一个新的缓存线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个int值的线程池
public static ExecutorService newSingleThreadExecutor() 创建一个线程池 相当于是int=1
如何实现线程池的代码:
\1. 创建一个线程池对象,控制要创建几个线程对象
public static ExecutorService newFixedThreadPool(int nThreads)
\2. 这种线程池的线程可以执行:
Runnable对象或者Callable对象代表的线程
做一个类实现Runnable接口
\3. 线程池调用方法:
Future<?> submit(Runnable task)
4.线程池结束:shutdown()方法
第三种实现线程的方式:实现Callable接口(是带泛型的接口)
这里指定的泛型是call()方法的返回值类型(这里的call()方法相当于那两个方法的run()方法)
好处:
可以有返回值
可以抛出异常
弊端:
代码比较复杂,必须依赖线程池,所以一般不用
匿名内部类方式使用多线程
继承Thread类来实现多线程:
new Thread(){代码…}.start();
实现Runnable接口来实现多线程:
new Thread(new Runnable(){代码…}){}.start();
(如果既继承了Thread类,又实现了Runnable接口,则输出是以继承的Thread类为主,但这是毫无意义的代码)
定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能
Timer类:定时
public Timer() 构造方法
public void schedule(TimerTask task, long delay) delay毫秒后执行任务
public void schedule(TimerTask task,long delay,long period) delay毫秒后执行任务,period毫秒后重复执行
public void cancel()
TimerTask类(抽象类):任务
public abstract void run()
public boolean cancel()
开发中(基本不用上两种方法,而是使用框架)
Quartz是一个完全由java编写的开源调度框架
sleep()和wait()的区别:
sleep():必须指时间;不释放锁
wait():可以不指时间,也可以指时间;释放锁
锁
共享锁(S锁): 又称为读锁,可以查看但无法修改和删除的一种数据锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据。资源共享.
排它锁(X锁): 又称为写锁、独占锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A
互斥锁: 在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
悲观锁、乐观锁:
悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。《LeetCode刷题Java版答案》pdf来了
乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
行级锁: 行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁: 表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
页级锁: 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
丢失修改: 指事务1和事务2同时读入相同的数据并进行修改,事务2提交的结果破坏了事务1提交的结果,导致事务1进行的修改丢失。
不可重复读: 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!
读脏数据: 事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤消,这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为”脏”数据,即不正确的数据。《LeetCode刷题Java版答案》pdf来了
死锁: 两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
死锁四个产生条件:
1)互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。