文章目录
线程、同步
一、线程
1. 并发与并行
- 并发:指两个或多个事件在同一时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9pJay0hv-1589461159618)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%B9%B6%E8%A1%8C.bmp)]
2. 进程概念
- **进程:**是指一个内存中运行的应用程序。
3. 线程概念
- **线程:**线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程是可以有多个线程的,这个应用程序也可以称为多线程程序。
4. 线程调度
- **分时调度:**所有线程轮流使用
cpu
的使用权,平均分配每个线程占用的cpu
的时间。 - 抢占式调度:优先让优先级高的线程使用
cpu
,如果线程的优先级相同,那么会随机选择一个,Java
使用的为抢占式调度。
5. 主线程
- **主线程:**执行主(
main
)方法的线程。 - 单线程程序:
java
程序中只有一个线程,执行从main
方法开始,从上到下依次执行。 JVM
执行main
方法,main
方法会进入到栈内存,JVM
会找操作系统开辟一条main
方法通向cpu
的执行路径,cpu
就可以通过这个路径来执行main
方法,而这个路径有一个名字就是main
(主)线程。
6. 创建多线程的第一种方式:创建Thread
类的子类
java.lang.Thread
类:是描述线程的类,想要实现多线程程序,就必须继承Thread
类。- 实现步骤:
- 创建一个
Thread
类的子类; - 在
Thread
类的子类中重写Thread
类中的run()
方法,设置线程任务; - 创建
Thread
类的子类对象; - 调用
Thread
类中的start()
方法,开启新的线程,执行run()
方法。void start()
是该线程开始执行,Java
虚拟机调用该线程的run()
方法。- 结果是两个线程并发的运行:当前进程(
main
)和另一个线程(创建的新线程,执行其run
方法)。 - 多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。
- 创建一个
java
程序属于抢占式调度,哪个线程优先级高,就先执行哪个线程;同一优先级,随机选择一个执行。
public class SubThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run " + i);
}
}
}
public class Test {
public static void main(String[] args) {
SubThread st = new SubThread();
st.start();
for (int i = 0; i < 20; i++) {
System.out.println("main " + i);
}
}
}
7. 多线程原理:随机性打印结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-leL7JXM8-1589461159621)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%9A%8F%E6%9C%BA%E6%80%A7%E6%89%93%E5%8D%B0%E7%BB%93%E6%9E%9C.bmp)]
8. 多线程原理:多线程内存图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiXn7Pol-1589461159622)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%86%85%E5%AD%98%E5%9B%BE%E8%A7%A3.bmp)]
9. Thread类的常用方法
-
获取线程名称的方法
- 第一种:使用
Thread
类中的方法getName()
String getName()
返回线程的名称。可以先获取到当前正在执行的线程,使用线程中的方法getName()
获取线程的名称。
- 第二种:
static Thread currentThread()
返回当前正在执行的线程对象的引用。
public class SubThread extends Thread { @Override public void run() { //第一种方式 /* String name = getName(); System.out.println(name);*/ //第二种方式 Thread thread = Thread.currentThread(); String name1 = thread.getName(); System.out.println(name1); } } public class Test { public static void main(String[] args) { SubThread st1 = new SubThread(); st1.start(); // thread-x SubThread st2 = new SubThread(); st2.start(); // thread-x SubThread st3 = new SubThread(); st3.start(); // thread-x Thread thread = Thread.currentThread(); String name = thread.getName(); System.out.println(name); // main } }
- 第一种:使用
-
设置线程名称的方法
- 第一种:使用
Thread
类中的方法setName(name)
。void setName(String name)
改变线程名称,使之与参数name
相同。
- 第二种:创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类给子线程起一个名字。
Thread(String name)
分配新的Thread
对象。
public class SubThread extends Thread{ public SubThread(){} public SubThread(String name) { super(name); } @Override public void run() { System.out.println(Thread.currentThread().getName()); } } public class Test { public static void main(String[] args) { SubThread st = new SubThread(); //第一种方法 st.setName("朱古力"); st.start(); // 朱古力 //第二种方法 new SubThread("猪猪侠").start(); // 猪猪侠 } }
- 第一种:使用
-
sleep
方法public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),毫秒数结束之后,线程继续执行。
public class Test { public static void main(String[] args) { for (int i = 1; i <= 60; i++) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
10. 创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
:该接口该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run
的无参数方法。java.lang.Thread
类的构造方法:public Thread(Runnable target)
分配新的Thread
对象。public Thread(Runnable target, String name)
分配新的Thread
对象。
- 实现步骤:
- 创建一个
Runnable
接口的实现类; - 在实现类中重写
Runnable
接口的run
方法,设置线程任务; - 创建一个
Runnable
接口的实现类对象; - 创建
Thread
类对象,构造方法中传递Runnable
接口的实现类对象; - 调用
Thread
类中的start
方法,开启新的线程执行run
方法。
- 创建一个
public class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run " + i);
}
}
}
public class Test {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
new Thread(runnable).start();
for (int i = 0; i < 20; i++) {
System.out.println("main " + i);
}
}
}
11. Thread与Runnable的区别
- 实现
Runnable
接口创建多线程程序的好处:- 避免了单继承的局限性
- 一个类只能继承一个类,类继承了
Thread
类就不能继承其他类。 - 实现了
Runnable
接口,还可以继承其他的类,实现其他的接口。
- 一个类只能继承一个类,类继承了
- 增强了程序的扩展性,降低了程序的耦合性
- 实现
Runnable
接口的方式,把设置线程任务和开启新线程进行了分离。 - 实现类中,重写了
run
方法,用来设置线程任务。 - 创建
Thread
类对象,调用start
方法,用来开启新线程。
- 实现
- 避免了单继承的局限性
12. 匿名内部类方式实现线程的创建
-
匿名:没有名字
-
内部类:写在其他类内部的类
-
匿名内部类的作用:简化代码
- 把子类继承父类,覆盖重写父类的方法,创建子类对象合成一步完成。
- 把实现类实现类接口,覆盖重写接口中的方法,创建实现类对象合成一步完成。
-
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。
-
格式:
new 父类/接口(){ 覆盖重写父类/接口中的方法 };
public class Test {
public static void main(String[] args) {
//父类
new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +" "+ i);
}
}
}.start();
//接口
Runnable runnable = new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +" "+ i);
}
}
};
new Thread(runnable).start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +" "+ i);
}
}
}).start();
}
}
二、同步
1. 线程安全问题概述
- 单线程程序不会出现线程安全问题。
- 多线程程序,没有访问共享数据,不会产生问题。
- 多线程程序访问了共享的数据,会产生线程安全问题。
2. 线程安全问题的代码实现
public class Tickets implements Runnable{
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
tickets--;
}
}
}
}
public class Test {
public static void main(String[] args) {
Runnable runnable = new Tickets();
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t0.start();
t1.start();
t2.start();
}
}
3. 线程安全问题出现的原理
- 线程安全问题是禁止出现的,可以让一个线程在访问共享数据的时候,无论该线程是否失去了
cpu
的执行权,都让其他的线程等待,等待该线程执行完,其他线程在运行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9MA4MIXB-1589461159623)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%E4%BA%A7%E7%94%9F%E7%9A%84%E5%8E%9F%E7%90%86.bmp)]
4. 解决线程安全问题:同步代码块
-
格式:
synchronized(锁对象){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
-
注意:
- 通过代码块中的锁对象,可以使用任意的对象。
- 必须保证多个线程使用的锁对象是同一个。
- 锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
public class Tickets implements Runnable{
private int tickets = 100;
//创建锁对象
Object obj = new Object();
@Override
public void run() {
while (true) {
//使用同步代码块
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
tickets--;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Runnable runnable = new Tickets();
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t0.start();
t1.start();
t2.start();
}
}
5. 同步技术的原理
- 同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
- 同步保证了只能有一个线程在同步中执行共享数据,保证了线程安全;但是程序会频繁的判断锁,获取锁,释放锁,程序的效率会降低。
6. 解决线程安全问题:同步方法
-
使用步骤:
- 把访问了共享数据的代码块抽取出来,放到一个方法中;
- 在方法上添加
synchronized
修饰符。
-
定义方法的格式:
修饰符 synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
-
同步方法的锁对象其实就是:实现类对象(
this
)。
public class Tickets implements Runnable{
private int tickets = 100;
//同步方法
public synchronized void sell(){
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
tickets--;
}
}
@Override
public void run() {
while (true) {
sell();
}
}
}
public class Test {
public static void main(String[] args) {
Runnable runnable = new Tickets();
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t0.start();
t1.start();
t2.start();
}
}
7. 静态同步方法
-
格式:
修饰符 static synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
-
静态的同步方法:锁对象不是
this
(this
是创建对象之后产生的,静态方法优先于对象),是本类的class
属性 -->class
文件对象(反射)。
8. 解决线程安全问题:Lock锁
java.util.concurrent.Locks.Lock接口
Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。Lock接口
中的方法:void lock()
获取锁void unlock()
释放锁
java.util.concurrent.locks.ReentrantLock implements Lock
- 使用步骤:
- 在成员位置创建一个
ReentrantLock
对象; - 在可能会出现安全问题的代码前调用
Lock
接口中的方法lock()
获取锁; - 在可能会出现安全问题的代码后调用
Lock
接口中的方法unlock()
释放锁。
- 在成员位置创建一个
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Tickets implements Runnable{
private int tickets = 100;
//创建ReentrantLock对象 多态
Lock rl = new ReentrantLock();
@Override
public void run() {
while (true) {
//调用lock方法
rl.lock();
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
tickets--;
}
//调用unlock方法
rl.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
Runnable runnable = new Tickets();
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t0.start();
t1.start();
t2.start();
}
}