文章目录
1.并发与并行
●并发:指两个或多个事件在同一时间段内发生。(交替执行)
●并行:指两个或多个事件在同一时刻发生。(同时发生)
即,并发就是你洗完澡后再听歌,并行就是你一边洗澡一边听歌。
2.进程与线程
(线程<进程)
●进程:程序的执行过程。(可在任务管理器查看)
●线程:进程中的一个执行单元。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
举例:Word的使用。
每次打开一个Word就相当于启动了一个进程,
在这个进程上又有许多其他程序在执行(例如:拼写检查,自动更正等),这些就是一个个线程。
如果Word关闭了,这些线程就会全部消失。但是如果这些线程消失了,Word不一定会消失。
线程一定得依附于进程才能够存在。
“同时”执行是线程给人的感觉,在线程之间实际上是轮换执行。
3.线程调度
(1)分时调度:
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
(2)抢占式调度:
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。java使用的为抢占式调度。
●设置线程的优先级:
打开任务管理器>选择希望设置优先级的进程>右键>转到详细信息>设置优先级
4.多线程的实现:
在Java中要想实现多线程的程序,必须依靠一个线程的主体类,即主线程(执行主方法(main)的线程)。然后此类继承Thread
类或实现Runnable
接口。
1)继承Thread类
java.lang.Thread
是操作线程的类,任何类只需要继承Thread
类就可以成为一个线程的主类。
Thread类下的两个重要方法:
run()
和start()
方法。
线程执行体:run()。(线程需要完成的任务)
线程的起点:start()。(线程的启动,启动后执行的方法体是run()方法定义的代码)
程序的起点:main()。
//1.创建一个Thread类的子类
public class MyThread extends Thread{
//2.重写Thread类中的run方法,设置线程任务
@Override
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("run:"+i);
}
}
}
public class doMain {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的start(),开启新的线程,执行run()
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:"+i);
}
}
}
随机性打印结果的原因:
多线程内存图解:
Thread类的常用方法:
getName() | 取得线程名字 |
---|---|
setName() | 设置线程名字 |
currentThread() | 取得线程名字 |
sleep(long millitime) | 使当前正在执行的程序以指定的毫秒数暂定 |
Thread.currentThread().getName() 和 this.getName()的区别
2)实现Runnable接口:
为了避免单继承局限的问题,我们可以使用Runnable
接口来实现多线程。
要启动多线程,就一定需要通过Thread
类中的start()
方法,但是Runnable接口中没有提供可以被继承的start()方法
。这时就需要借住Thread
类中提供的有参构造方法:
public Thread(Runnable target) | 此方法可以接收一个Runnable接口对象 |
---|
//MyThread是实现了Runnable接口的子类
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
new Thread(mt).start();
new Thread(mt2).start();
3)实现Runnable接口的好处:
①避免单继承局限;
②降低程序的耦合性,方便解耦。即把设置线程任务(实现类中重写run())和开启新线程(Thread类对象调用start())进行了分离。
4)多线程的两种实现方式及区别:
●它们的实现都需要一个线程的主类,都必须在子类中覆写run()
方法,都必须调用Thread
类中的start()
方法来开启线程。
●Thread
类是Runnable
接口的子类,而使用Runnable接口可以避免单继承局限,方便解耦,并且可以更加方便地实现数据共享的概念。
public class Thread extends Object implements Runnable
●它们的结构:
Runnable接口 | Thread类 |
---|---|
class MyThread implements Runnable{} | class MyThread extends Thread{} |
new Thread(mt).start(); | mt.start(); |
5)使用匿名内部类实现多线程的创建:
ublic class doMain {
public static void main(String[] args) {
//1.线程的父类是Thread
new Thread(){
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+">--"+"a");
}
}
}.start();
//2.线程的接口Runnable
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"b");
}
}
}).start();
}
}
5.线程的操作状态:
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有以下五种状态:
- 创建状态
新线程对象处于新建状态时 - 就绪状态
新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。 - 运行状态
当就绪的线程被调度并获得CPU资源时,便进入运行状态。此时,自动调用该线程对象的run()方法,run()方法定义了线程的操作和功能 - 堵塞状态
在某种特殊情况下,被人为挂起或执行输入输出操作时,将让出 CPU 并临时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以进入就绪状态。 - 终止状态
线程调用stop()或run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的功能。
6.线程的休眠与优先级:
1)线程的休眠:
是让程序执行速度变慢一些。在Thread类中线程休眠操作方法为:
public static void sleep(long millis) throws InterruptedException
设置的休眠单位是毫秒(ms)。
2)线程的优先级:
对高优先级,使用优先调度的抢占式策略。哪个线程的优先级高,哪个线程就有可能被执行。
- MAX_PRIORITY : 10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
线程优先级操作方法:
setPriority(int p)
:设置线程的优先级getPriority()
:取得线程优先级
7.线程安全问题(同步与死锁):
先来解释不同步遇到的问题:
如果分成三个窗口卖100张票,假如不同步的话,就有可能出现三个窗口卖重票、错票的情况。(多个线程操作同一资源可能出现的情况,因为前面的线程还没完成操作,其它线程也进来操作车票。(抢占))
实现三个窗口来卖票的程序:
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true){
if (ticket>0) {
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}
}else {
break;
}
}
}
public class doMain {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
解决方法:通过同步操作来解决。
- 同步操作:一个代码块中的多个操作在同一时间段内只能由一个线程进行,其他线程要等待此线程完成后才可以继续执行。
以下有三种方式完成同步操作:
其实就是添加上图中的“锁”。
1)同步代码块:
synchronized(this){
//需要被同步操作的代码
}
关于this的解释:
- 在实现Runnable接口创建多线程的方式中,我们可以使用this充当所,代替手动new一个对象,因为后面我们只创建一个线程的对象。
- 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。我们可以使用当前类来充当这个是锁。synchronized (类名.class)
使用同步代码块完成同步操作:
主要有变化的在run()方法里,doMain类不变。
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
while(true){
//先判断票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}else {
break;
}
}
}
}
}
2)同步方法:
利用synchronized定义的方法。
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
while(true){
sale();
}
}
public synchronized void sale(){
//先判断票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
补充:
- 非静态同步方法,锁对象:this
- 静态同步方法,锁对象:RunnableImpl.class
3)Lock锁:
java.util.concurrent.locks.Lock接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
Lock接口中的方法:
void Lock() | 获取锁 |
---|---|
void unlock() | 释放锁 |
Lock接口的实现类:ReentrantLock
java.util.concurrent.locks.ReentrantLock implements Lock
使用Lock锁完成同步操作:
三步走。1.创建ReentrantLock对象 2.获取锁 3.释放锁
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//1.创建ReentrantLock对象
Lock lk = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
while(true){
//2.在可能出现线程安全的代码前调用Lock接口中的Lock方法获取锁.
lk.lock();
if (ticket>0) {
try {
Thread.sleep(10);
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能出现线程安全的代码后释放锁。
lk.unlock();
}
}
}
}
}
4)比较 synchronized 与 Lock:
- synchronized是自动释放锁(显示),lock需要手动释放和关闭锁(隐式)。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- Lock只有代码块锁,synchronized有代码块锁和方法锁
5)总结:
加入同步后明显比不加入同步慢许多,所以同步的代码性能低,但是数据安全性高。
6)常见面试题分析:
-
同步和异步有什么区别。什么情况下使用?
如果一块数据要在多个线程间共享,则必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,那么就应该用异步编程,在很多情况下采用异步途径往往更有效率。 -
abstract的method是否可以同时是static,是否可以同时是native、synchronized?
method、static、native、synchronized都不能和“abstract”同时声明方法。 -
当一个线程进入一个对象的synchronized方法后,其他线程是否可访问此对象的其他方法?
不能访问,一个对象操作一个synchronized方法只能由一个线程访问。(其他线程等待)