Java——多线程
一、概述
进程:
正在运行的程序,是系统进行资源分配和调用的独立单位
每一个进程都有它自己的内存空间和系统资源
线程:
是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一条执行路径,则为单线程程序
一个进程如果有多条执行路径,则称为多线程程序
java程序运行原理
java命令会启动java虚拟机,JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程再去调用某个类的main方法。
所以main方法运行在主线程中。在此之前的所有线程都是单线程的。
二、多线程的实现
java提供了一个类描述线程 Thread,一个JVM中可以创建多个线程同时执行,每个线程都有优先权。
java中创建线程对象的几种方式:
1.自己写一个A类继承Thread类,重写run方法,这个A类就叫线程类
2.自己写一个A类实现Runnable接口,实现run方法,这个A类就叫线程类
3.自己写一个A类实现Callable接口,实现call方法,这个A类就叫线程类,需要结合线程池才能使用
线程实现方式1:自己写一个A类继承Thread类,重写run方法,这个A类就叫线程类
注意:
1.run方法就是一个线程对象要做的事情
2.线程对象不能靠调用run方法来启动线程,只能算普通的对象调用了一个普通的方法,和线程没关系了
3.要想启动一个线程,应该调用start()来进行启动,JVM进程会为这个线程开辟一个对象,这个线程内部调用对应的run方法
4.调用完start()方法之后,线程只是具备了执行的资格,但是还没有真正的执行,只有抢到了CPU执行权的时候,才会执行
(执行run方法里的逻辑,run方法执行完了,线程也就结束了)
Thread构造方法
Thread()分配一个新的Thread对象
Thread(String name)分配一个新的Thread对象
Thread类中的成员方法
public final String getName() 获取当前线程的名字
public final void setName(String name) 将此线程的名称更改为等于参数name 。
public static Thread currentThread() 获取当前方法所在的线程 主线程的名字叫做main
MyThread1 t1 = new MyThread1(); // 在使用无参构造方法的时候,给创建的对象起了名字
MyThread1 t1 = new MyThread1("xxx"); //使用带参数的构造方法,在创建线程对象的时候给线程起名字
三、线程调度
每个线程都存在优先级
线程有两种调度模型:
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用的CPU的时间片
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一点
Java使用的是抢占式调度模型
public final int getPriority() 获取线程的优先级
默认情况下,线程的优先级都是5
public final void setPriority(int newPriority) 修改线程的优先级 范围是[1,10]
优先级高的线程只是说先执行的概率会大一些,并不是一定会执行
四、线程控制
public static void sleep(long millis) 休眠线程

public final void join() 其他线程会等待该线程执行结束 加入线程

礼让线程:public static void yield() 为了让线程之间更加和谐

后台线程(守护线程):
线程分为两种:
用户线程:是没有设置Daemon的线程
守护线程:当没有用户线程的时候守护线程自动死亡

中断线程:
public final void stop() 直接将线程停止,已被弃用
public void interrupt()

四、生命周期

五、Runnable接口(多线程的实现2)
自己写一个A类实现Runnable接口,实现run方法,这个A类就叫线程类,需要借助Thread类来创建线程对象
因为是实现了Runnable接口,接口中只有一个抽象run方法,找不到getName()方法,所以需要间接的获取当前线程对象,然后再获取线程名字
System.out.println(Thread.currentThread().getName()+" -- "+i);
实现接口的好处
可以避免由于java单继承带来的局限性
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离
如何创建线程对象并启动呢?
MyRunnable myRunnable = new MyRunnable();
需要借助Thread类来创建线程对象 public Thread(Runnable target)
Thread t1 = new Thread(myRunnable);
t1.setName("大哥");
public Thread(Runnable target, String name) 创建线程对象的同时给线程起名字
Thread t1 = new Thread(myRunnable, "大哥");
六、多线程程序练习、



为了模拟真实的收票场景,可以在售票过程中加入延迟
加入延迟之后出现的问题:
1、出现了售卖重复的票的现象。原因:CPU的一次操作必须是原子性的,CPU小小的时间片可以执行很多次
2、出现了售卖第0张票和第-1张票的现象。原因:线程的执行具有随机性
其实出现的现象就是线程不安全的现象
如何判断是否存在线程安全的问题?三个缺一不可
1.是否存在多线程环境?符合
2.是否存在共享变量(共享数据)?符合
3.是否存在多条语句操作着共享变量(共享数据)?符合
如何解决线程安全的问题?
1.synchronized 同步代码块
2.加锁
同步代码块:
java给我们提供了一个关键字给我们使用:synchronized
定义格式:
synchronized(对象){
操作共享数据的代码
}
对象有个要求:必须是多个线程共享对象且唯一的


同步方法的锁对象是谁?
是当前整在执行的锁对象自身 this


同步的特点:
前提
多个线程
多个线程使用的是同一个锁对象
好处
解决了多线程的安全问题
弊端
当线程较多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步静态方法的锁对象是谁?
是当前类的class文件对象。 

七、Lock锁
解决多线程安全问题的方案之一:使用Lock锁来实现
Lock本身是一个接口,所以找一个实现类:ReentrantLock
void lock()加锁
void unlock()释放锁


死锁问题
同步弊端
效率低
如果出现了同步嵌套,就容易产生死锁问题
死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象



八、等待唤醒机制
共享数据:学生对象
生产者(线程):给学生对象成员变量赋值
消费者(线程):取出学生对象的成员变量值并打印
测试类:StudentDemo
问题1:我们正常编写代码后发现,运行的结果是null---0 这是必然的结果
因为消费者中拥有一个只属于自己的学生对象,而不是与生产者共享的
解决方案:只需要在外面new出来,以参数的形式传入到生产者和消费者中
问题2:我们将问题1进行了修改,将学生对象放在了外部创建,但是运行的时候发现,还是会有null---0的结果出现,这是偶然
这是因为线程之间执行具有随机性导致的,解决方案:为了看的更加明显,我加入循环
问题3:我们在问题2的基础之上加入了循环,让它赋不同的值,让结果看的更加丰富,于是我们又发现出现了学生的姓名和年龄对应不上的现象。
这其实是线程安全的问题。
判断是否存在线程安全的问题三要素:
1、是否存在多线程环境?是
2、是否存在共享数据?存在
3、是否存在多条语句操作着共享数据?存在
解决方案:同步代码块 到这里只是解决了姓名和年龄不匹配的问题,依旧没有解决问题2的null--0问题
问题4:如何解决依旧存在null---0的问题
这是一个等待唤醒机制的问题(建立线程安全的基础之上)
生产者
作为生产者,应该先 看一看数据有没有被消费,如果被消费才会生产
如果上一次的数据没有被消费,应该等待消费者消费,等待的同时要通知消费者将上一次生产的数据进行消费


消费者
作为消费者,在消费数据之前应该先看一看有没有数据,如果有数据才消费
如果没有数据,等待生产者生产数据,同时也要通知生产者生产数据

也要创建一个共享数据类:学生对象
还有测试类要创建生产者线程对象和消费者线程对象
SetStudentThread setStudentThread = new SetStudentThread(student);
GetStudentThread getStudentThread = new GetStudentThread(student);
九、线程组
java中使用ThreadGroup来表示线程组,它可以对一批线程组进行分类管理,java允许程序直接对线程组进行控制
默认情况下,所有的线程都属于主线程组
public final ThreadGroup getThreadGroup()
我们也可以给线程设置分组
Thread(ThreadGroup group,Runnable target,String name)
构造方法
ThreadGroup(String name)构造一个新的线程组
创建一个线程组对象
ThreadGroup tg1 = new ThreadGroup("帅哥组");
ThreadGroup tg2 = new ThreadGroup("美女组");
如何将一个线程放入到一个线程组中? 以Thread类构造方法的方式指定
Thread(ThreadGroup group, String name)
分配一个新的 Thread对象。
创建一个线程对象
MyThread t1 = new MyThread(tg1,"xxx");
MyThread t2 = new MyThread(tg1,"xxx");
获取线程组的名字
1、先获取线程所在的线程组
2、再获取线程组的名字
System.out.println(t1.getThreadGroup().getName());
System.out.println(t2.getThreadGroup().getName());
tg1.setDaemon(true); 直接对组进行操作,也会影响到组中的线程,有了线程组,方便管理和分类
十、线程池
程序启动一个新线程的成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建打来那个生存期很短的线程时,
更应该考虑使用线程池。
线程池例的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对像来使用。
方法
static ExecutorService newCachedThreadPool() 创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。
static ExecutorService newSingleThreadExecutor() 创建一个使用从无界队列运行的单个工作线程的执行程序。
static ExecutorService newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。
创建一个固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2); 2指的是同一时刻最多只有两个线程执行
线程一旦被放入了线程池中就会开始执行(相当于调用了start()方法,具备了执行资格)
Future<?> submit(Runnable task)
提交一个可运行的任务执行,并返回一个表示该任务的未来。
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
线程池中的线程之间会互相抢CPU执行权
fixedThreadPool.submit(new MyRunnable()); 这里底层相当于有一个Thread类包装成线程对象了并且也开始执行
fixedThreadPool.submit(new MyRunnable()); 这里底层相当于有一个Thread类包装成线程对象了并且也开始执行
fixedThreadPool.submit(new MyRunnable()); 这里底层相当于有一个Thread类包装成线程对象了并且也开始执行
线程池需要手动关闭,一般情况下需要不断地放入线程执行,一般不需要关。
fixedThreadPool.shutdown();
<T> Future<T> submit(Callable<T> task) 创建线程第三种方式
提交值返回任务以执行,并返回代表任务待处理结果的Future。
fixedThreadPool.submit(new MyCallable());
fixedThreadPool.submit(new MyCallable());
fixedThreadPool.submit(new MyCallable());
十一、Callable接口(多线程的实现3)
好处
可以有返回值
可以抛出异常
弊端
代码比较复杂,所以一般不用

十二、匿名内部类方式使用多线程
new Thread(){代码...}.start();
NEW Thread(new Runnable(){代码...}.start();
使用匿名内部类创建线程对象并启动
new 类/接口/抽象类(){
重写方法
}




十三、定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。
在java中可以通过Timer和TimerTask类来实现定时调度功能
构造方法
Timer() 创建一个新的定时器
创建一个定时器对象
Timer timer = new Timer();
调用方法定时,告诉定时器到了时间该干什么
void schedule(TimerTask task, long delay) 在指定的延迟之后安排指定的任务执行。
第一个参数指的是到时间要做的事情,第二个参数是延迟多久单位是毫秒
timer.schedule(new MyTask(timer),5000);
timer.cancel();
void schedule(TimerTask task, long delay, long period) 在指定的延迟之后开始 ,重新执行固定延迟执行的指定任务。
timer.schedule(new MyTask(),10000,2000);


浙公网安备 33010602011771号