一、java中实现多线程的三种方式
(1)继承Thread类的方式进行实现;
(2)实现Runnable接口的方式进行实现;
(3)利用Callable接口和Future接口方式实现。
1.继承Thread类的方式进行实现
步骤:
自定义一个类继承Thread;重写run()方法,run()方法中写的是想要线程执行的代码;创建子类的对象,并启动线程。
例如:
这里使用线程的getName()方法和setName()方法对线程进行区分。
2.实现Runnable接口的方式进行实现
自己定义一个类实现Runnable接口;重写里面的run()方法;创建子类的对象;创建一个Thread类的对象并开启线程。
例如:
说明:上述代码中实现了Runnable接口的类是放在了新建的Thread类中的,可以理解为实现了Runnable接口的类里面是线程要执行的任务,将任务交给执行线程。还需要注意,为了区分线程,需要给线程取名setName(),但是在实现了Runnable接口的类的run()方法不能直接使用getName()来获取线程名字,它是Thread类中的方法,解决方式是使用Thread的静态方法currentThread()来获取执行run()的当前线程,然后再使用getName()方法。
应用场景:你可能会觉得Runnable很麻烦,但要注意,java中继承只有一个父类,而实现接口可以多个,且实现接口也可以继承,所以当有继承的类想要实现多线程时就可以使用Runnable。
3.利用Callable接口和Future接口方式实现
可以发现前两种线程的实现方式中,run()都没有返回值,不能获取线程执行的结果。
步骤:创建一个类实现Callable接口;重写call(),该方法有返回值,返回线程运行的结果;创建实现Callable接口的类的对象(表示多线程要执行的任务);创建FutureTask的对象(管理多线程执行的结果);创建Thread类的对象并启动。
例如:
说明:上图中Callable的泛型中指定的数据类型是重写的call()的返回值类型。
说明:上面定义的是FutureTask的对象,通过泛型指定管理的返回值结果的类型。实现Callable接口的类可以看成指定任务,将其对象作为FutureTask对象的构造器参数,再将FutureTask对象给Thread对象的构造器参数,最后通过Thread对象来开启线程,而获取线程的执行结果是通过FutureTask对象获得。
二、Thread常用的成员方法
说明:线程的优先级越高,抢占到cpu的概率越大。
1.getName()和setName()
getName()和setName()在之前的代码中演示过了。需要注意的是
(1)如果没有用使用setName()为线程设置名字,使用getName()会获取线程默认的名字,格式为:Thread-x(x=0,1,…)。
(2)除了通过setName()的方式为线程设置名字之外,还可以在新建Thread对象时为线程设置名字。
如果使用继承了Thread的类,需要重写父类的构造方法(子类不继承父类的构造方法)。
例如:
2.Thread.currentThread()
前面使用过了。
注意:在main方法中使用Thread.currentThread().getName()后打印方法返回值可以发现值为“name”,在JVM虚拟机启动后,会自动启动多条线程,其中一条线程叫做main线程,它的作用就是调用main方法,执行里面的代码。
3.sleep()
方法说明:
线程执行到这个方法会停留对应的时间(由参数指定,单位为毫秒,1秒=1000毫秒),当时间到了之后,线程会自动地醒来,继续执行后续代码。
例如,下图中的代码的效果为先输出11111,等待5秒后再输出00000:
4.setPriority()和getPriority()
设置和获取线程的优先级的方法。注意,线程的优先级默认为5,最小为1,最大为10。
例如:
注意:线程的优先级并不说明一定率先占用cpu先执行完,只是概率更高。
5.setDaemon()与守护线程
守护线程即备胎线程,当其他的非守护线程执行完毕后,守护线程也会陆续结束。
例如,对设置线程优先级的代码进行改写:
可以看到执行结果,当“飞机”线程结束后,“备胎线程”也会慢慢结束,还没有输出到99就结束了。
6.yield()和出让线程
Thread().yield()让出cpu,注意只是出让,之后还是会去再抢夺。
7.join()和插入线程
yield()和join()用的少,不过多说明。
三、同步代码块
同步代码块:把操作共享数据的代码锁起来。
格式:
synchronized(锁){
操作共享数据的代码
}
锁对象需要是唯一的,用static关键字修饰即可。
特点:
(1)锁默认打开,有一个线程进去了,锁自动关闭;
(2)里面的代码全部执行完毕,线程出来,锁自动打开。
例如,在不使用同步代码块的情况下,一个3个窗口同时贩票的情况下:
可以看到贩票不按顺序,且有的票重复,有的没有,这里涉及相同的同步机制就不过多解释。
使用java中的同步代码块机制。
上图中圈出的部分即为同步代码块(将其置于synchroized()后的花括号中,obj为锁,可任意设置,只要有static修饰)不同线程执行该代码块时其他线程不能执行。
执行结果如下:
补充,锁一般使用当前类的字节码对象,是唯一的:
补充一个小细节:不同区域的同步代码块的锁相同,也只能有一个线程执行,不能分别进入不同区域。
例如:
上例中,两个继承Thread类的子类中的同步代码块用了同一把锁,此时会发现,只能是一个一个线程执行完run()方法里的内容后,另一个线程才能执行run()里的内容。
而如果无锁则两个区域的代码并发执行:
说明:
就是想说明一把锁不仅可以锁住同一个代码区,还可以一起锁住多个代码区。
四、同步方法
同步方法是指被synchronized关键字修饰的方法。
格式:
特点:
(1)同步方法是锁在方法里面所有的代码;
(2)锁对象不能自己指定。java已经规定好,非静态的方法锁对象是自身所在类,静态的同步方法的锁对象是所在类的字节码文件对象。
例如:
说明:这里要注意一下,使用实现Runnable的类来为线程设置任务时,ticket不用设置为静态变量,因为在下图中的代码中将实现Runnable的类的一个对象分别给了不同的Thread对象。
补充:StringBuilder和StringBuffer的不同之处就在于StringBuilder是线程不安全的,它的成员方法都没有synchronized关键字修饰,而StringBuffer是线程安全的,它的成员方法都由synchronized关键字修饰。
五、锁
jdk5之后提供了一个锁接口Lock,实现了比使用synchronized方法和语句更广泛的锁定操作,提供了手动获得锁和释放锁的方法。注意Lock是接口不能直接实例化,要通过它的实现类ReentrantLock来进行操作。
例如:
注意:上图中的代码有一个问题,就是程序一直运行没有终止,原因是当有一个线程进入同步块上锁后,当ticket==201时(票售罄),此时直接执行break;语句跳出循环,没有释放锁,而其他两个线程还处于while()循环中等着上锁,一直没有终止。解决方法是将释放锁的语句放到try-catch-finally的finally里,如下: