简述java多线程
目录
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即 指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过 程:有它自身的产生、存在和消亡的过程。
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。 若一个程序可同一时间执行多个线程,就是支持多线程的
多线程程序的优点:
-
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
-
提高计算机系统CPU的利用率
-
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
创建线程的两种方式
一、继承Thread类
-
定义子类继承Thread类。
-
子类中重写Thread类中的run方法。
-
创建Thread子类对象,即创建了线程对象。
-
调用线程对象start方法:启动线程,调用run方法
相关API:
start():1.启动当前线程2.调用线程中的run方法
run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():主动释放当前线程的执行权
a.join():在线程中插入执行另一个线程a,该线程被阻塞,直到插入执行的a线程完全执行完毕以后,该线程才继续执行下去
stop():过时方法。当执行此方法时,强制结束当前线程。
sleep(long millitime):使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行
isAlive():判断当前线程是否存活
public class Thread1 extends Thread{
@Override
public void run()
{
for (int i = 0; i < 100; i++) {
if (i % 2==0)
{
System.out.println(Thread.currentThread().getName() + ": "+i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
for (int i = 100; i < 200; i++) {
if (i % 2!=0)
{
System.out.println(Thread.currentThread().getName() + ": "+i);
}
}
}
}
Thread-0: 0
main: 101
Thread-0: 2
main: 103
Thread-0: 4
main: 105
Thread-0: 6 //多线程产生了
Thread实现任务的局限性
- 任务逻辑写在Thread类的run方法中,有单继承的局限性
- 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享
二、实现Runnable接口
1)定义子类,实现Runnable接口。
2)子类中重写Runnable接口中的run方法。
3)通过Thread类含参构造器创建线程对象。
4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
5)调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
public class PrimeRun implements Runnable {
//线程执行体
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest3 {
public static void main(String[] args) {
PrimeRun pr = new PrimeRun();
//新建线程
Thread t1 = new Thread(pr);
//启动线程
t1.start();
Thread t2 = new Thread(pr);
t2.start();
for (int i = 100; i < 200; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
main:117
Thread-1:66
main:119
Thread-1:68
main:121
Thread和Runnable的区别
【区别】
继承Thread: 线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
若多个线程需要访问共享数据时,首选使用实现 Runnable 接口的方式
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。
三、额外增加的方式
实现 Callable 接⼝
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "abc";
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable c = new MyCallable();
FutureTask<String> f = new FutureTask<>(c);
Thread th = new Thread(f);
th.start();
System.out.println(f.get());
}
}
abc
callable和runnable的区别
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
可以通过FutureTask类获取返回结果
线程的调度
调度策略:
时间片:线程的调度采用时间片轮转的方式
抢占式:高优先级的线程抢占CPU
Java的调度方法:
1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略
有关方法:
- static void yield():线程让步 ➢暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 ➢若队列中没有同优先级的线程,忽略此方法
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 低优先级的线程也可以获得执行
- static void sleep(long millis):(指定时间:毫秒)
- ➢令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
- ➢抛出InterruptedException异常
- boolean isAlive():返回boolean,判断线程是否还活着
- interrupted:对象.interrupted用于打断sleep,join,yield
Thread t1 = new Thread(h);//h为实现了Runnable的类实例
t1.start();
while(t1.isAlive()){
t1.interrupt();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
try {
t1.join(3000);
} catch (InterruptedException e) {
}
Thread.yield();
线程的生命周期 (牢记)
JDK中用Thread.State类定义了线程的几种状态 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
-
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
-
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
-
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
-
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
-
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的优先级
线程的优先级控制
-
MAX_PRIORITY(10);
-
MIN _PRIORITY (1);
-
NORM_PRIORITY (5);
涉及的方法:
-
getPriority() :返回线程优先值
-
setPriority(int newPriority) :改变线程的优先级
-
线程创建时继承父线程的优先级
线程的同步
多个线程执行的不确定性引起执行结果的不稳定
多个线程对账本的共享,会造成操作的不完整性,会破坏数据
例如:
模拟火车站售票程序,开启三个窗口售票
class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true)
{
if (ticket > 0)
{
System.out.println(Thread.currentThread().getName()+": "+--ticket);
}
else
{
break;
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
t2窗口: 2
t2窗口: 1
t2窗口: 0
t3窗口: 23
t1窗口: 32
看,出现线程问题了,t2窗口为显示票数为 0 后,t1 和 t3窗口依然可以卖票
1.问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
2.解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
解决方式:
1)同步代码块:
synchronized (同步监视器){
// 需要被同步的代码;
}
同步监视器:俗称“锁”,可以使用任意类型的对象充当。但是必须保证多个线程持有同一把锁(同一个对象)
Object obj=new Object();
@Override
public void run() {
while (true)
{
synchronized (obj) //obj作为锁
{
if (ticket > 0)
{
System.out.println(Thread.currentThread().getName()+": "+--ticket);
}
else
{
break;
}
}
}
}
t1窗口: 22
t1窗口: 21
t3窗口: 20
t3窗口: 19
t3窗口: 18
2)同步方法 :
synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (){ …. }
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + --ticket);
}
}
使用synchronized关键字把共享数据包装起来,确保一次只有一个线程执行流访问共享数据
3)同步锁 Lock
while(true){
l.lock();//上锁
try{
if(tick > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " 完成售票,余票为:" + --tick);
}
}finally{
l.unlock();//释放锁
}
总结:
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
- 同步方法的锁:静态方法(类名.class)、非静态方法(this)
- 同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就
无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)
问题指引:
1、如何找问题,即代码是否存在线程安全?
1明确哪些代码是多线程运行的代码
2明确多个线程是否有共享数据
3明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决?
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
3、切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃
自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
解决方法
-
专门的算法、原则
-
尽量减少同步资源的定义
-
尽量避免嵌套同步
线程的通信
等待唤醒机制:
wait() : 使当前“同步监视器”上的线程进入等待状态,同时释放锁
notify()/notifyAll() : 唤醒当前“同步监视器”上一个/所有等待状态的线程
注意:上述方法必须使用在 同步中
wait() 方法
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或 notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行
notify()/notifyAll()
在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
例子:
使用两个线程打印 1-100. 线程1, 线程2 交替打印
思路:当一个线程运行程序结束后,先让该线程进入等待状态,下一个进程进入时再把前面等待状态的线程唤醒
public class NotifyTest implements Runnable{
int i=0;
Object object=new Object();
@Override
public void run() {
while (true)
{
synchronized (object)
{
object.notify();
if (i < 100){
System.out.println(Thread.currentThread().getName()+":"+ ++i);
}
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
先写到这吧,并发这一块真的太多东西要写了,我也只是初步的接触一下,后面肯定还会再补回来的,回头见…