Java基础之多线程
记一次失败的学习方式
线程的三中创建方式,先上代码:
/*
创建新线程的三种方式:
1、继承Thread类;
2、实现Runable接口;
3、实现Callable接口;
需求:创建多线程对象,开启多线程。在子线程中输出1-100之间的偶数,主线程输出1-100之间的奇数。
*/
public class Test9 {
public static void main(String[] args) {
/*
线程启动必须调用start()方法,而非run()
//方式一
ThreadTest1 tt1 = new ThreadTest1();
tt1.setPriority(10);//设置线程优先级,默认为5,范围1-10
tt1.start();
*/
/*
//方式二
ThreadTest2 tt2 = new ThreadTest2();
Thread theTt2Thread = new Thread(tt2);
theTt2Thread.start();
*/
//方式三
Thread tt3 = new Thread(){
@Override
public void run(){
//使用匿名类可以很方便的访问外部变量(这里并未访问外部局部变量)
//但是在JDK7以前,就必须使用final修饰
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println("子线程:" + i);
}
}
};
//tt3.setPriority(10);//线程优先级太高导致子线程打印完主线程才打印┓( ´∀` )┏233333(主线程默认5)
tt3.start();
//同时,该匿名类可以使用lambda表达式简化
Thread tt4 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println("子线程:" + i);
}
});
//主线程打印奇数
for (int i = 0; i < 100; i++) {
if (i % 2 == 1) System.out.println("主线程:" + i);
}
}
}
//通过继承Thread类来创建新线程
class ThreadTest1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println("子线程:" + i);
}
}
}
//通过实现Runnable接口来创建新线程
class ThreadTest2 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println("子线程:" + i);
}
}
}
最开始执行该代码时,会产生这样的结果:
子线程居然在主线程打印完了才开始打印,这曾一度让我以为没有创建出新线程,刚开始的时候start()方法在主方法下,我以为是顺序的原因,所以我先调用子线程的start()方法,然后在主方法打印(也就是现在的代码),发现结果还是这样,我甚至以为是因为ThreadTest方法不是公共的原因,然后想到之前看C#的时候有讲到优先级问题,然后百度了一下发现了具体原因:
结果居然变了
子线程居然又开始工作了,我花了一晚上查线程优先级问题,最后发现最开始的情况居然复现不了了???(绝对不是主线程for循环在上面的原因)不行我得把我查到的结论整理一下o(╥﹏╥)o,等以后再遇到这种情况再说吧
线程优先级
1.在任意时刻,当有多个线程处于可运行状态时,运行系统总是挑选一个优先级最高的线程执行,只有当线程停止、退出或者由于某些原因不执行的时候,低优先级的线程才可能被执行
2.两个优先级相同的线程同时等待执行时,那么运行系统会以round-robin的方式选择一个线程执行(即轮询调度,以该算法所定的)(Java的优先级策略是抢占式调度!)
3.被选中的线程可因为一下原因退出,而给其他线程执行的机会:
1) 一个更高优先级的线程处于可运行状态(Runnable)
2)线程主动退出(yield),或它的run方法结束
3)在支持分时方式的系统上,分配给该线程的时间片结束
4.Java运行系统的线程调度算法是抢占式(preemptive)的,当更高优先级的线程出现并处于Runnable状态时,运行系统将选择高优先级的线程执行
5.例外地,当高优先级的线程处于阻塞状态且CPU处于空闲时,低优先级的线程也会被调度执行参见这里
放个很详细的博客,不过有些地方还不是特别明白,以后再仔细研读
线程状态及同步处理
线程的状态,这里有个线程状态图:
其实上面这张图并不是特别完整,下面对于状态的转换会更完整一点:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed_Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
有三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制。
直接上代码:
线程同步来模拟过山洞
/*
模拟多个人通过一个山洞:
1.这个山洞每次只能通过一个人,每个人通过山洞的时间为1秒;
2.随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过隧道的人数。
显示每次通过山洞人的姓名,和通过顺序;
*/
public class Test9 {
public static void main(String[] args) {
/*
Hole hole1 = new Hole();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(hole1);
t.start();
}
*/
//使用锁
int numOfCrossed = 0;
int[] theTest = new int[1];
//锁必须是同一个,下面synchronized的参数同理,关于synchronized参数并没有限制
Lock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.lock();
crossTheHole(theTest);
lock.unlock();
}).start();
}
}
public static void crossTheHole(int[] numOfCrossed){
String name = Thread.currentThread().getName();
System.out.println(name+"正在穿过洞");
try {
Thread.sleep(600);
}catch (InterruptedException e){
e.printStackTrace();
}
numOfCrossed[0]++;
System.out.println(name+"穿过了山洞,他是第"+ numOfCrossed[0] +"个");
}
}
class Hole implements Runnable{
int numOfCrossed = 0;
Object lock = new Object();
@Override
public void run(){
crossTheHole2();
}
//同步代码块
public void crossTheHole(){
synchronized (lock){
String name = Thread.currentThread().getName();
System.out.println(name+"正在穿过洞");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
numOfCrossed++;
System.out.println(name+"穿过了山洞,他是第"+ numOfCrossed +"个");
}
}
//同步方法
public synchronized void crossTheHole2(){
String name = Thread.currentThread().getName();
System.out.println(name+"正在穿过洞");
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
numOfCrossed++;
System.out.println(name+"穿过了山洞,他是第"+ numOfCrossed +"个");
}
}
线程通信及锁
进程间通信使用wait()和notify()进行等待和唤醒,注意使用的锁需要是同一个,并且线程被唤醒后并不是立即进入运行状态,而是从WAITING释放与其他线程进行竞争,如果获取到锁,则进入RUNNABLE状态,否则进入BLOCKED状态(即未获取到锁)。
TIMED_WAITING在API中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态,最常见的就是sleep()方法,当然,sleep单独一个线程也可以使用,注意sleep()并不会释放当前锁权限
关于wait()和notify():
Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
每个对象都有一个机锁来控制同步访问。Synchronized关键字可以和对象的机锁交互,来实现线程的同步。
由于sleep()方法是Thread 类的方法,因此它不能改变对象的机锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程仍然无法访问这个对象。而wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。
Yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield() 方法将不会起作用。
一个线程结束的标志是:run()方法结束。
一个机锁被释放的标志是:synchronized块或方法结束。
Wait()方法和notify()方法:当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去了对象的机锁。当它被一个notify()方法唤醒时,等待池中的线程就被放到了锁池中。该线程从锁池中获得机锁,然后回到wait()前的中断现场。
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。
值得注意的是:线程的在被激活后不一定马上就运行,而是进入到可运行线程的队列中。
另外,Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。
关于多线程的更多信息请参见我的另一篇博文:Java多线程之以7种方式让主线程等待子线程结束,里面用到了concurrent这个强大的多线程包。
synchronized:
synchronized同步代码块时锁可以使任意对象
synchronized同步非静态方法时,锁是this
(因此同一对象的多个同步方法不能同时执行)
synchronized同步静态方法时,锁是该类的类对象,即类名.class
(即多个同步静态方法不能同时执行)