1 多线程
1.1多线程介绍
1.1.1 进程的概念
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
1.1.2 线程的概念
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
每一个线程都有一个做自己的栈空间,栈空间是线程私有的.
堆是多个线程共享的.
1.3 线程的调度模式
1.2 main线程
每一个程序里面至少要包含一条线程。我们之前写的java程序也是一样的。
当JVM启动时,会给当前程序创建一个执行路径,这个执行路径就是一个线程,叫做main线程。
如果希望在程序中可以同时执行多个任务,那么可以使用多线程。
在java中有一个类,表示线程类,这个类叫做Thread。
我们可以通过这个去创建新线程,然后并运行。
1.3 Thread类
1.3.1 并发与并行
并发:同时执行多个线程,这个同时并不是真正意义上的同时。因为CPU在多个线程之间切换的速度非常快,所以可以看成同时。
并行:同时执行多个线程,这个同时指的就是真正意义上的同时。同一个时间点,多个线程同时执行。
1.3.2 多线程实现方式一:使用一个类去继承Thread类。
使用步骤:
1. 新建一个类,去继承Thread类。
2. 重写run方法。 新线程执行的时候会运行这个run方法,所以需要把线程要执行的任务代码写在run方法中。
3. 在测试类中创建Thread子类对象
4. 调用线程的start方法,启动这个线程。
void start(): 新线程开始执行,并且JVM会调用这个线程的run方法。
(1)线程执行流程图解
(2)多线程实现的三个小问题:
为什么要继承Thread类?<br /> 在java中Thread类里面封装了很多线程相关的方法,是一个线程类。<br /> 当我们使用一个类继承这个类的时候,这个子类也就变成了线程类。<br /> <br /> 为什么要重写run方法?<br /> 因为新线程运行的时候会执行run方法,所以我们需要在run方法中定义线程要执行的任务。<br /> 如果不重写run方法,会执行父类Thread类的run方法,而父类的run里面的内容并不是我们想要的。<br /> <br /> 为什么要调用start方法而不是run方法。<br /> start: 会运行新线程,并且在新线程中运行run方法。<br /> run: 会在原来的线程中运行run,而不是在新线程中运行run
(3)线程的内存图
1.2 Runnable接口
1.2.1 多线程实现方式二:实现Runnable接口的方式(重要,最常用)
步骤:<br /> 1. 定义一个类,实现Runnable接口。(这个类不是线程类,因为这个类目前和Thread并没有什么关系。一般把这个类称为线程任务类,因为它实现了Runnable接口要 重写里面的run方法,我们需要在run方法中定义线程要执行的任务。)<br /> 2. 重写run方法,定义线程要执行的任务。新线程会执行run方法。<br /> 3. 在测试类中创建这个类(Runnable接口的实现类)的对象<br /> 4. 创建Thread类的对象,在构造方法中传递Runnable接口的实现类对象。<br /> 5. 调用start方法运行新线程。
/*
这个类不是线程类,因为这个类目前和Thread并没有什么关系。
一般把这个类称为线程任务类,因为它实现了Runnable接口要重写里面的run方法,我们需要在run方法中定义线程要执行的任务。
*/
public class MyRunnableImpl implements Runnable {
//定义线程要执行的任务
//在控制台打印100次Hello java
@Override
public void run() {
System.out.println("Hello...java...start");
for(int i = 0; i < 1000; i++) {
System.out.println("Hello...java:" + i);
}
}
}
public class Demo03Runnable {
public static void main(String[] args) {
//在测试类中创建这个类(Runnable接口的实现类)的对象
MyRunnableImpl mri = new MyRunnableImpl();
//创建Thread类的对象,在构造方法中传递Runnable接口的实现类对象。
Thread t = new Thread(mri);
//调用start方法,运行新线程。
t.start();//启动新线程,会执行mri这个对象中的run方法。
//打印100次Hello...php
System.out.println("hello...php...start");
for(int i = 0; i < 1000; i++) {
System.out.println("hello...php");
}
}
}
1.2.2 多线程有两种实现方式比较
1. 继承Thread类。<br /> 2. 实现Runnable接口<br /> <br /> 第二种方式更好<br /> 好处:<br /> 1. 解决了java中类与类之间只能单继承的局限性。<br /> 2. Runnable接口里面只有一个run方法,没有其他的start,setName, getName这些方法。Runnable接口的功能比较单一,我们只需要在里面关注线程任务就可以了。符合java设计模式 单一职责原则。<br /> 3. 降低了耦合性。
1.3 匿名内部类实现多线程(了解,不推荐这么写,可阅读性太低)
匿名内部类创建的其实是子类的对象.
package cn.itcast.demo02_多线程的基本使用;
/*
使用匿名内部类的方式实现多线程。
格式:
new 父类或接口() {
重写的方法;
}
匿名内部类其实创建的是子类的对象。
*/
public class Demo04InnerThread {
public static void main(String[] args) {
//继承Thread类的方式完成多线程
/*
new Thread() {
public void run() {
System.out.println("新线程执行了");
}
}.start();
*/
//匿名内部类 -> 实现Runnable接口的方式
/*
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("新线程执行了");
}
};
//创建Thread对象,把Runnable接口实现类对象作为参数传递。
new Thread(r).start();
*/
//创建Thread对象,把Runnable接口实现类对象作为参数传递。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程执行了");
}
}).start();
}
}
1.4 Thread类中的方法
Thread类中有很多方法,可以对线程进行操作。
String getName(): 获取线程的名字。
void setName(String name): 设置线程的名字。
(重要)static Thread currentThread(): 获取当前正在运行的线程对象。
static void sleep(long millis): 线程休眠。 参数为毫秒值。(1秒 = 1000毫秒)
2 线程安全问题
如果多个线程共享同一个数据,在操作这个数据的时候就可能会引发线程安全问题。
2.1 产生bug的原因:
1.多线程是抢占式执行,任意时刻都有可能被别的线程抢走CPU的执行权(程序员无法控制)
2.多个线程使用了共享数据(只需要对共享数据进行控制即可)
2.2 线程安全解决方式
2.2.1 方式一:同步代码块
介绍:就是java提供的一种解决线程安全问题的方式<br /> <br /> 在代码块声明上 加上synchronized<br /> 格式:<br /> synchronized (任意对象) {<br /> 可能会产生线程安全问题的代码<br /> }<br /> 任意对象俗称锁对象,专业术语叫对象监视器<br /> 注意事项<br /> 1.需要保证多个线程使用同一个锁对象<br /> 2.同步代码块必须包裹在第一次使用共享数据的地方
synchronized 可以解决线程安全问题。
这个单词表示的意思是同步,可以加在代码块上面,也可以加在方法上面。
如果把synchronized加在代码块,那么他就是同步代码块。
格式:
synchronized(锁对象) {
//…
}
锁对象: 就是一个普通的对象,可以是Object对象,也可以是其他任意类型的对象。专业的叫做对象监视器。
锁对象的作用: 只有持有锁的线程,才能进入到同步代码块中。
public class Ticket2 implements Runnable{
int ticket = 100;
//创建一个对象,这个对象表示锁对象,除此之外没有任何含义
Object lock = new Object();
@Override
public void run() {
while(true) {
//当线程执行到同步代码块的时候会进行判断。
//判断这个同步代码块上还有没有锁对象, 如果有锁,就获取到这个锁,然后进入到同步代码块。
// 如果这个代码块上没有锁,那么就一直在这里等着获取锁。如果一直获取不到锁对象,那么就一直等着。
synchronized (lock) {//lock 是锁对象,只有持有锁的线程才能够进入到同步代码块。
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket);
ticket--;
}
}//当线程离开同步代码块,那么就会释放掉这个锁。 释放掉锁之后,其他线程就可以去竞争这个锁,谁抢到了这个锁,谁就进入到同步代码块。
}
}
}
2.2.2 方式二:同步方法
也是java提供的一种解决线程安全问题的方式
在方法声明上加上synchronized
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法相当于把整个方法体都加上了同步代码块。
同步方法也是有锁的。
如果这个方法是非静态的,那么这个锁对象是this。
如果这个方法是静态的, 也有锁,这个锁是 当前类.class(反射的时候会说)
同步代码块中的锁。多个线程一定要用同一个锁对象。
同步代码块和同步方法都可以保证线程安全
同步代码块用起来更加的灵活。
同步方法用起来更加的简洁。
2.2.3 lock接口(了解)
在jdk1.5之后,提供了一个包,叫做java.util.concurrent。这个包下面的类和接口很多都是对线程进行操作的。
其中有一个Lock接口。可以提供手动获取锁,以及释放锁的操作。
方法:
void lock():获取锁
void unlock(): 释放锁。
接口不能直接使用,所以我们可以使用Lock接口下面的其中一个实现类ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket4 implements Runnable{
int ticket = 100;
//创建Lock锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
//调用lock对象的lock方法手动获取锁
lock.lock();
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket);
ticket--;
}
//调用unlock释放锁
lock.unlock();
}
}
}