要了解java多线程,首先需要我们理清楚一个问题:
什么是线程,什么是进程?
进程是一个应用程序。
线程是一个进程中的执行场景/执行单元。
当然一个进程也可以启动多个线程。
线程调度:
-
抢占式调度模型:
那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。 -
均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
有一些编程语言,线程调度模型采用的是这种方式。
关于java线程的生命周期:
在java中,实现多线程有两种方式:
1.编写一个类,继承java.lang.Thread。
2.实现java.lang.Runnable接口。
java线程构造方法
构造方法名 备注 Thread() Thread(String name) name为线程名 Thread(Runnable target) Thread(Runnable target,String name) name为线程名这两种方法都必须重写run()方法,不同的是,当线程启动时,第一个方法可以直接调用线程对象的start()方法,而第二种方法需要使用实现Runnable接口的对象作为参数构造一个Thread类的对象,并调用start()方法。
eg.
// 定义一个类继承Thread类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();
注意:
t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
// 定义一个类实现Runnable接口
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
注意:
实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。
run()方法不可以throws向上抛出异常,只能在方法中使用try--catch进行异常处理
获取当前线程对象、线程对象名字的获取和修改
方法名 作用 static Thread currentThread() 获取当前线程对象 String getName() 获取线程对象名字 void setName(String name) 修改线程对象名字线程没有被手动设置名字时,其默认的名字为:
Thread-0
Thread-1
Thread-2
...
以此类推
线程的sleep方法
方法名 作用 static void sleep(long millis) 休眠当前线程millis毫秒注意:
静态方法:Thread.sleep(1000);
参数是毫秒
作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
eg.
public class ThreadTest06 {
public static void main(String[] args) {
// 睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
关于线程的interrupt()方法
方法名 作用 void interrupt() 中断线程注意:
interrupt()不能中断在运行中的线程,它只能改变中断状态而已。实际完成的是让受阻塞的线程退出阻塞状态。
关于线程的yield()方法
方法名 作用 static void yield() 让位方法,使当前线程暂停,让出CPU时间片,回到就绪状态。注意:
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”,在回到就绪之后,有可能还会再次抢到。
eg.
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
for(int i = 1; i <= 10000; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyRunnable6 implements Runnable {
@Override
public void run() {
for(int i = 1; i <= 10000; i++) {
//每100个让位一次。
if(i % 100 == 0){
Thread.yield(); // 当前线程暂停一下,让给主线程。
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
关于线程的join()方法
方法名 作用 void join() 将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行到终止 void join(long millis) 同上,等待该线程终止的时间最长为millis毫秒 void join(long millis,int nanos) 同上,等待该线程终止的时间最长millis毫秒+nanos纳秒eg.
public class ThreadTest13 {
public static void main(String[] args) {
System.out.println("main begin");
Thread t = new Thread(new MyRunnable7());
t.setName("t");
t.start();
//合并线程
try {
t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable7 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
方法名 | 作用 |
void wait() | 让活动在当前对象的线程无线等待(释放之前占有的锁) |
void notify() | 唤醒当前对象正在等待的线程(并不会释放锁) |
void notifyAll() | 唤醒当前对象全部正在等待的线程(并不会释放锁) |
注意:
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是 Object 类中的。wait方法和notify方法不是通过线程对象调用,
多线程并发环境下数据安全问题:
当出现如下条件时,会出现数据安全问题:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
线程安全问题的解决方法:
线程排队执行,用排队执行解决线程安全问题。
这种机制被称为:线程同步机制,实际上就是线程不能并发了,线程必须排队执行。
但是这种方法会使线程无法并发,会牺牲效率。
为解决这类问题,java提供了一个关键字:synchronized
多线程互斥的实现
对于访问某个关键共享资源的所有方法,都必须把它们设为 synchronized,例如:
synchronized void f() {
/* ... */
}
synchronized void g() {
/* ... */
}
当一个线程A使用一个synchronized修饰的方法时,其它线程想使用这个方法时就必须等待,直到线程A 使用完该方法 (除非线程A主动让出CPU资源)。
如果想保护某些资源不被多个线程同时访问,可以强制通过 synchronized方法访问那些资源。
▪ 调用synchronized方法时,对象就会被锁定
▪ 当synchronized方法执行完或发生异常时,会自动释放锁
▪ 被synchronized保护的数据应该是私有(private)