多线程
一、多线程概述
进程:
•正在运行的程序,是系统进行资源分配和调用的独立单位。
•每一个进程都有它自己的内存空间和系统资源。
线程:
•是进程中的单个顺序控制流,是一条执行路径
•一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序
在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。 线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。 单线程:如果程序只有一条执行路径。 多线程:如果程序有多条执行路径。
多线程有什么意义呢? 多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。 程序的执行其实都是在抢CPU的资源,CPU的执行权。 多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
java运行原理:
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
思考:jvm虚拟机的启动是单线程的还是多线程的?
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
并行和并发?
并行:是逻辑上同时发生,指在某一个时间内同时运行多个程序;
并发:是物理上同时发生,指在某一个时间点同时运行多个程序;
二、多线程的实现
2.1、如何实现呢?
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。
那么Java提供的类是什么呢?
Thread
通过查看API,我们知道了有2中方式实现多线程程序。
方式1:继承Thread类。
步骤
A:自定义类MyThread继承Thread类。
B:MyThread类里面重写run()
目的:将自定义代码存储在run方法,让线程运行
C:创建对象
D:调用线程的start方法:
该方法有两步:启动线程,调用run方法。
为什么重写run()方法呢?
不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码
实现多线程:
创建一个类,继承Thread类
public class MyThread extends Thread {
public MyThread() {
}
//用于修改线程名字
public MyThread(String name){
super(name);
}
多线程
// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//my1.run();调用run()方法是单线程的
my1.start();
my2.start();
调用run()方法为什么是单线程的呢?
因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
run()和start()的区别?
run():仅仅是封装被线程执行的代码,直接调用是普通方法
start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
方式二:实现Runnable接口
步骤:
A:自定义类MyRunnable实现Runnable接口
B:重写run()方法
C:创建MyRunnable类的对象
D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
public class MyRunnable implements Runnable {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
// 创建Thread类的对象,并把C步骤的对象作为构造参数传递
// Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
// t1.setName("线程一");
// t2.setName("线程二");
// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "线程一");
Thread t2 = new Thread(my, "线程二");
t1.start();
t2.start();
方式3、通过Callable和Future创建线程:
实现步骤:①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
1 public class CallableFutrueTest {
2 public static void main(String[] args) {
3 CallableTest ct = new CallableTest(); //创建对象
4 FutureTask<Integer> ft = new FutureTask<Integer>(ct); //使用FutureTask包装CallableTest对象
5 for(int i = 0; i < 100; i++){
6 //输出主线程
7 System.out.println(Thread.currentThread().getName() + "主线程的i为:" + i);
8 //当主线程执行第30次之后开启子线程
9 if(i == 30){
10 Thread td = new Thread(ft,"子线程");
11 td.start();
12 }
13 }
14 //获取并输出子线程call()方法的返回值
15 try {
16 System.out.println("子线程的返回值为" + ft.get());
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 } catch (ExecutionException e) {
20 e.printStackTrace();
21 }
22 }
23 }
24 class CallableTest implements Callable<Integer>{
25 //复写call() 方法,call()方法具有返回值
26 public Integer call() throws Exception {
27 int i = 0;
28 for( ; i<100; i++){
29 System.out.println(Thread.currentThread().getName() + "的变量值为:" + i);
30 }
31 return i;
32 }
33 }
三种方法对比:
继承Thread:线程代码存放在Thread子类run方法中。
优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
劣势:已经继承了Thread类,无法再继承其他类。
实现Runnable:线程代码存放在接口的子类的run方法中。
优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
实现Callable:
优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法
2.2、修改线程名字
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//方法一:调用方法设置名称
// my1.setName("线程一");
// my2.setName("线程二");
// my1.start();
// my2.start();
//方法二:带参构造方法给线程起名字 需要在MyThread设置带参构造方法
// MyThread my1 = new MyThread("线程一");
// MyThread my2 = new MyThread("线程二");
// my1.start();
// my2.start();
获取main方法所在的线程对象的名称
针对不是Thread类的子类中获取线程对象名称
public static Thread currentThread():返回当前正在执行的线程对象
Thread.currentThread().getName()
System.out.println(Thread.currentThread().getName());
2.3、线程调度和线程控制
线程调度:
线程有两种调度模型:
分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
获取线程对象的优先级
public final int getPriority():返回线程对象的优先级,默认是5
设置线程对象的优先级
public final void setPriority(int newPriority):更改线程的优先级。
注意:
-
线程默认优先级是5。
-
线程优先级的范围是:1-10。
-
线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
Student stu = new Student();
Student stu1 = new Student();
stu.setName("线程1");
stu1.setName("线程2");
//获取线程优先级
System.out.println("线程优先级默认为:"+stu.getPriority());
//设置线程优先级
stu.setPriority(4);
stu1.setPriority(8);
stu.start();
stu1.start();
线程控制
线程休眠 public static void sleep(long millis)
线程加入 public final void join()
线程礼让 public static void yield()
后台线程 public final void setDaemon(boolean on)
中断线程
public final void stop() 已弃用
public void interrupt()
线程休眠:
线程睡眠的原因:线程执行的太快,或需要强制执行到下一个线程。
线程睡眠的方法(两个):sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。
sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。
public class ThreadSleep extends Thread {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ThreadSleep ts3 = new ThreadSleep();
ts1.setName("线程一");
ts2.setName("线程二");
ts3.setName("线程三");
ts1.start();
ts2.start();
ts3.start();
每隔一秒打印一次

扩展:Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
线程加入
Thread类的Join()措施能够将两个交替厉行的线程并合为次序厉行的线程。例如在线程B中调用了线程A的Join()措施,线程A将插入线程B之前,直到线程A执行告终后,才会继续执行线程B。
join可以用来临时加入线程执行。
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("李渊");
tj2.setName("李世民");
tj3.setName("李元霸");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
注意:join()必须在该线程start()方法之后,其他线程start()之前执行才能正常使用
线程礼让
该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。
public class ThreadYield extends Thread {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();
ty1.setName("线程一");
ty2.setName("线程二");
ty1.start();
ty2.start();
后台线程
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
如果所有的前台线程都死亡,后台线程会自动死亡
Student stu1 = new Student();
Student stu2 = new Student();
stu1.setName("关羽");
stu2.setName("张飞");
//设置为守护线程
stu1.setDaemon(true);
stu2.setDaemon(true);
stu1.start();
stu2.start();
Thread.currentThread().setName("刘备");
for (int x = 0; x < 2000; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}

当前台线程执行完毕,只剩下后台线程时,即使后台线程没有执行完毕,也会立即停止;
因为停止程序有点时间,所以还会在执行一点点。
中断线程
public final void stop():让线程停止,过时了,但是还可以使用。
public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。
public class ThreadStop extends Thread {
ThreadStop ts = new ThreadStop();
ts.start();
// 三秒后中断线程
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

三、线程的生命周期

四、线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。
synchronized(对象){ 对象可以是任意对象,但要求多个线程必须是同一个对象,可以写this
需要同步的代码;
}
synchronized可以同步代码块,也可以同步方法;
注意:
-
同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
-
多个线程必须是同一把锁。
同步的前提
•多个线程
•多个线程使用的是同一个锁对象
同步的好处
•同步的出现解决了多线程的安全问题。
同步的弊端
•当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
•容易产生死锁问题
4.1、同步代码块
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
追加问题:如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!
静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象。 类名.class 该对象类型是Class。
所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class。代码如下:
public static mySyn(String name){
synchronized (Xxx.class) {
Xxx.name = name;
}
}
4.2 同步方法
public synchronized void run(){
...
}
4.3、JDK5中Lock锁的使用
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock:
void lock(): 获取锁。
void unlock():释放锁。
ReentrantLock是Lock的实现类.
public class SellTicket implements Runnable {
// 定义票
private int tickets = 100;
// 定义锁对象
private Lock lock = new ReentrantLock();
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个窗口
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
五、死锁
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLockTest(true));
Thread t2 = new Thread(new DeadLockTest(false));
t1.start();
t2.start();
}
}
class DeadLockTest implements Runnable{
private boolean flag;
static Object obj1 = new Object();
static Object obj2 = new Object();
public DeadLockTest(boolean flag) {
this.flag = flag;
}
public void run(){
if(flag){
synchronized(obj1){
System.out.println("if lock1");
synchronized (obj2) {
System.out.println("if lock2");
}
}
}else{
synchronized (obj2) {
System.out.println("else lock2");
synchronized (obj1) {
System.out.println("else lock1");
}
}
}
}
}
六、匿名内部类方式使用多线程
// 继承Thread类来实现多线程
new Thread() {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":"
+ x);
}
}
}.start();
// 实现Runnable接口来实现多线程
new Thread(new Runnable() {

