文章目录
- 前言
- 多线程
- 多线程的实现
- ①、继承Thread类
- Thread常用方法
- Thread的构造器
- 优缺点
- ②、实现Runnable接口
- 优缺点
- ③、实现Callable接口
- Runnable接口和继承Thread类的区别
- 线程同步机制
- volatile
- 同步锁
- 同步方法
- lock锁
- 线程池
前言
线程(thread)是一个程序内部的一条执行路径。
我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
多线程是指从软硬件上实现多条执行流程的技术
或者说多线程是指多个可以同时并发运行的程序
消息通信、淘宝、京东系统等都离不开多线程技术。
多线程
并发:在一个时间段出现的,可能不是同时发生
并行:同时发生
进程:一个任务创建、运行、消亡
线程:进程的一个单元,一个进程可以有多个线程。
多线程的实现
多线程实现方式有3种:
继承Thread类
实现Runnable接口重写run方法(方法无返回值 )
实现Callable接口,重写call方法(方法可以有返回值 )
①、继承Thread类
① 定义一个子类,重写run方法
② 实例化子类
③ 对子类对象执行start
方法来启动线程
(直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法才是启动一个新的线程执行。)
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();//2.实例化子类,创建Thread的子类对象
myThread.start(); //3.对子类对象执行start方法来启动线程
for(int i =0; i<10; i++) {
System.out.println("主线程"+i);
}
}
}
class MyThread extends Thread{ //1.自定义一个子类,重写run方法
@Override
public void run(){
for(int i =0; i<10; i++) {
System.out.println("子线程"+getName()+i); //getName()返回线程名称
}
}
}
Java是通过java.lang.Thread 类来代表线程的。
按照面向对象的思想,Thread类应该提供了实现多线程的方式。
Thread常用方法
String getName() //获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) //将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称
public static Thread currentThread(): //返回对当前正在执行的线程对象的引用
public static void sleep(long time) //让当前线程休眠指定的时间后再继续执行,单位为毫秒(Thread类的线程休眠方法)
public void run() //线程任务方法
public void start() //线程启动方法
注意:
1、此方法是Thread类的静态方法,可以直接使用Thread类调用。
2、这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
Thread的构造器
public Thread(String name) //可以为当前线程指定名称
public Thread(Runnable target) //封装Runnable对象交给线程对象
public Thread(Runnable target,String name) //封装Runnable对象成为线程对象,并指定线程名称
优缺点
优点:编码简单
缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展。不能返回线程执行结果
②、实现Runnable接口
① 声明一个Runnable接口实现类,重写run方法
② 实例化,把实例化对象作为线程的目标进行创建
③ 执行start方法来启动线程
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();//2.实例化
Thread t = new Thread(myThread);//2.把实例化对象myThread作为Thread的target进行创建
t.start();//启动线程
for(int i =0; i<10; i++) {
System.out.println("主线程"+i);
}//主线程和子线程在抢夺资源,所以每次运行结果都不一样
}
}
//1.声明一个Runnable接口实现类,重写run方法
class MyThread implements Runnable{
@Override
public void run() {
for(int i =0; i<10; i++) {
System.out.println("子线程"+Thread.currentThread().getName()+i); //getName()返回线程名称
}
}
}
优缺点
优点:线程任务类只是实现了Runnale接口,可以继续继承类和实现接口,扩展性强
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的
③、实现Callable接口
与Thread类、Runnable接口对比它是可以有返回值
public class Demo3 implements Callable<String>{
//实现call方法
@Override
public String call() throws Exception {
System.out.println("Demo3开始运行");
Thread.sleep(3000); //暂停3000毫秒
System.out.println("Demo3运行结束");
return "二哈喇子";
}
}
测试类:
public class 实现Callable接口线程测试 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Demo3 d3 = new Demo3();
//创建FutureTask类对象 获得线程执行后返回值
FutureTask<String> ft = new FutureTask<>(d3);
//创建Thread类对象
Thread thread = new Thread(ft);
thread.start();
String result = ft.get();
System.out.println("Demo3执行完的结果是"+result);
System.out.println("main执行结束");
}
}
Runnable接口和继承Thread类的区别
这个问题面试的时候经常问
① 、创建线程的方式不同
② 、设置线程名方式不同
③、获取线程名方式不同
④ 、由于Java是单继承,一个类继承Thread类以后不能继承其他类,扩展性不好。而实现Runnable接口则可以侧面实现了多继承
⑤、继承Thread类不能实现线程变量资源共享,注意,是线程里的变量实现Runnable 接口线程变量是可以共享的,也可以不共享,看创建线程的方式
线程同步机制
模拟电影院卖100张票
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);//线程1
Thread t2 = new Thread(myThread);//线程2
t1.start();
t2.start();
}
}
class MyThread implements Runnable{
private int num =100;//定义一个共享票源
@Override
public void run() {
while(num > 0) {//还有票
try {
Thread.sleep(50);//为了模拟买票操作耗时
System.out.println(Thread.currentThread().getName()+"正在卖"+num+"票");//获取当前线程名
num--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程安全问题是由全局变量及静态变量引起的,若多线程同时执行写操作(单纯读操作一般是线程安全的),线程会去抢夺cpu资源完成操作,造成线程不安全。
同步机制:
1、volatile
2、同步锁
3、同步方法
4、CAS
5、Lock锁
synchronized(同步锁){
需要同步操作的代码:1锁对象可以是任意类型。2多个线程对象要使用同一把锁。
}
public synchronized void method(){
可能会产生线程安全问题的代码
}
class Ticket implements Runnable{
Lock lock = new ReentrantLock();
private int num = 100;//定一个多线程共享的票源
@Override
public void run() {
while (true){
//上锁
lock.lock();
if (num>0){
...//代码省略
}
//释放锁
lock.unlock();
}
}
}
volatile
假如说线程1修改了data变量的值为1,然后将这个修改写入自己的本地工作内存。
那么此时,线程1的工作内存里的data值为1。
然而,主内存里的data值还是为0!线程2的工作内存里的data值还是0啊?!
作用:
1、一旦data变量定义的时候前面加了volatile来修饰的话,那么线程1只要修改data变量的值,就会在修改完自己本地工作内存的data变量值之后,强制将这个data变量最新的值刷回主内存,必须让主内存里的data变量值立马变成最新的值!
2、如果此时别的线程的工作内存中有这个data变量的本地缓存,也就是一个变量副本的话,那么会强制让其他线程的工作内存中的data变量缓存直接失效过期,不允许再次读取和使用了!
3、如果线程2在代码运行过程中再次需要读取data变量的值,他就必须重新从主内存中加载data变量最新的值!那么不就可以读取到data = 1这个最新的值了!
同步锁
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);//线程1
Thread t2 = new Thread(myThread);//线程2
t1.start();
t2.start();
}
}
class MyThread implements Runnable{
private int num =100;//定义一个共享票源
Object obj = new Object();
@Override
public void run() {
while(true) {//窗口永远开启
synchronized(obj) {//同步锁
if(num > 0) {
try {
Thread.sleep(50);//为了模拟买票操作耗时
System.out.println(Thread.currentThread().getName()+"正在卖"+num+"票");//获取当前线程名
num--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
同步方法
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);//线程1
Thread t2 = new Thread(myThread);//线程2
t1.start();
t2.start();
}
}
class MyThread implements Runnable{
private int num =100;//定义一个共享票源
Object obj = new Object();
@Override
public void run() {
while(true) {//窗口永远开启
method();
}
}
public synchronized void method(){ //同步方法
if(num > 0) {
try {
Thread.sleep(50);//为了模拟买票操作耗时
System.out.println(Thread.currentThread().getName()+"正在卖"+num+"票");//获取当前线程名
num--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
CAS(Compare and Set)
lock锁
使用ReentrantLock实现同步, lock()方法上锁,unlock()方法释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);//线程1
Thread t2 = new Thread(myThread);//线程2
t1.start();
t2.start();
}
}
class MyThread implements Runnable{
private int num =100;//定义一个共享票源
Lock lock = new ReentrantLock(); //声明一个lock对象
@Override
public void run() {
while(true) {//还有票
lock.lock(); //上锁
if(num > 0) {
try {
Thread.sleep(50);//为了模拟买票操作耗时
System.out.println(Thread.currentThread().getName()+"正在卖"+num+"票");//获取当前线程名
num--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock(); //释放锁
}
}
}
线程池
问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
好处:
1、降低资源消耗,如果线程池无空闲,则需等待其他任务执行完毕后归还线程。
2、提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3、提高线程的可管理性,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*1.创建线程池对象
2.实现Runnable接口子类
3.使用线程池
4.关闭线程池*/
public class Test {
public static void main(String[] args) {
//1.创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2); //包含2个线程对象
MyThread m = new MyThread(); //2.实现Runnable接口子类
// Thread t = new Thread(m);
// t.start();
service.submit(m);//3.使用线程池
service.submit(m);//submit执行之后程序并没有关闭,是因为线程池控制了线程的关闭
service.submit(m);
// service.shutdown();//4.关闭线程池
// service.submit(m); //关闭之后再执行则报异常
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("售票员开始卖票");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}//为了模拟买票操作耗时
System.out.println(Thread.currentThread().getName()+"正在卖票");//获取当前线程名
System.out.println("线程结束,回到线程池");
}
}