Java多线程(上)
多线程的引入
什么是线程
- 线程是程序执行的一条路径,一个进程可以包含多多条线程。
- 多线程并发执行可以提高程序效率,可以同时完成多项工作
并行与并发的区别
- 并行就是两个任务同时执行,这需要多核cpu
- 并发是指两个任务都请求执行,但是处理器只能接受一个任务,就把这两个任务轮流执行,由于时间间隔较短,就感觉是两个任务在同时执行。
多线程的程序实现方式
1. 继承Thread
定义类继承Thread
重写run方法
把新线程要做的事写在run方法中
创建线程对象
开启新线程,内部会自动执行run方法
public class Demo_Thread2 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
}
}
2. 实现Runnable
public class Demo_Thread2 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t = new Thread(mt);
t.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
}
}
两种方式的区别
1.源码的区别
1.继承Thread:由于子类重写了Thread类的run方法,当调用start时,直接找到子类的run方法
2. 实现Runnable:构造函数中传入了Runnable引用,成员变量记住了它,start调用run方法时,内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run,运行时执行的是子类的run
2
- 继承Thread
- 好处:可以直接使用Thread类中的方法,代码简单
- 弊端:如果已经有了父类则无法使用这种方法
- 实现Runnable
- 好处:即使自己定义的线程类有了父类也没关系,可以再实现接口,并且接口是可以多实现的
- 弊端:不能直接使用Thread中的方法,必须要先获取到线程对象,才能得到Thread的方法
匿名内部类实现线程的方式
1.继承Thread
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
}
}.start();
}
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
}
};
t.start();
}
2.实现Runnable
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
}
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
}
});
t.start();
}
Runnable r = new Runnable() {
@Override
public void run() {
}
};
Thread t2 = new Thread(r);
t2.start();
获取名字和设置名字
获取名字
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"...ccc");
new Thread(){
@Override
public void run() {
System.out.println(getName()+"...bbb");
}
}.start();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"...aaa");
}
});
t.start();
}
//输出:
main...ccc
Thread-0...bbb
Thread-1...aaa
可以看出,默认主线程的名字为main,而其他线程的名字默认为Thread-n
设置名字
1. 通过构造方法
2. 通过setName()方法
线程睡眠:sleep
sleep
让当前正在执行的线程暂停一段时间,并进入阻塞状态
当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的 机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行。
yield
此外,Thread还提供了一个与sleep()方法有点相似的yield()静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让线程暂停一下
实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。
关于sleep()与yield()方法的区别
- sleep()方法暂停当前线程之后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同或者优先级更高的线程执行机会。
- sleep()方法会将线程转入阻塞状态,知道经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,他只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后,立即再次获得处理器资源被执行
- sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显示抛出该异常;而yield()方法则没有声明抛出任何异常
- sleep()方法比yield()方法有更好的可移植性
守护线程(后台线程)
守护线程一般为其他线程提供服务,守护线程的特征:如果所有的非守护线程(前台线程)都死亡,后台线程会自动死亡。
调用Thread的setDaemon(true) 方法可将指定的线程设置成守护线程。
Thread还提供了一个isDaemon()方法来判断指定线程是否是守护线程。
前台线程死亡后,jvm会通知后台线程死亡,但从它接收指令到做出响应,需要一定的时间。而且要将某个线程设置为后台线程时,必须在该线程启动前设置。
加入线程
Thread提供了让一个线程等待另外一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。
设置线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
Thread提供了setPriority(),getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是1~10之间的一个整数,也可以使用Thread类提供的三个静态常量
- MAX_PRIORITY:其值是10
- MIN_PRIORITY:其值是1
- NORM_PRIORITY:其值是5
同步代码块
1.什么情况下需要同步
- 当多线程并发,有多段代码同时执行时,我们希望某一段代码在执行过程中cpu不要切换到其他线程工作,这时需要同步。如果两段代码是同步的,那么同一时间段只能执行一段,在一段代码没执行结束之前不会执行另外一段代码
2.怎样同步
- 使用synchronized关键字加上一个锁对象(可以是任意对象),这就叫同步代码块,多个代码块如果使用相同的锁对象,那么他们是同步的。
public class Demo_Thread {
public static void main(String[] args) {
final Printer p = new Printer();
new Thread(){
public void run(){
while(true){
p.print1();
}
}
}.start();
new Thread(){
public void run(){
while(true){
p.print2();
}
}
}.start();
}
}
class Printer {
Demo d =new Demo();
public void print1() {
synchronized (d) {
System.out.print("安");
System.out.print("卓");
System.out.print("开");
System.out.print("发");
System.out.println();
}
}
public void print2(){
synchronized (d) {
System.out.print("前");
System.out.print("端");
System.out.print("工");
System.out.print("程");
System.out.print("师");
System.out.println();
}
}
}
class Demo{}
当一个线程执行print1()方法时,执行到一半时,cpu转向另一个线程执行print2()方法时,发现锁对象被第一个线程占用,所以无法执行此线程的print2()方法,当cpu转向第一个线程继续执行完,释放锁对象,恰好cpu要转向另一个线程执行print2()方法时,这时可以获取锁对象,便可以执行了。
同步方法
与同步代码块对应,同步方法就是使用synchronized 关键字修饰某个方法,则该方法成为同步方法。对于synchronized 修饰的实例方法(非静态)而言,无需显示指定同步监视器,同步方法的同步监视器就是 this,也就是调用该方法的对象。这样便可以在多线程并发访问一个对象时,只有一个线程能够调用同步方法。
不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。
释放同步监视器的锁定
线程安全问题
多线程并发操作同一数据,就有可能出现线程安全问题,用同步解决。
死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁。
public class Demo_DeadLock {
private static String s1 ="筷子左";
private static String s2 ="筷子右";
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
while(true){
synchronized (s1){
System.out.println("获取"+s1+"等待"+s2);
synchronized (s2){
System.out.println("拿到"+s2+"开吃");
}
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while(true){
synchronized (s2){
System.out.println("获取"+s2+"等待"+s1);
synchronized (s1){
System.out.println("拿到"+s1+"开吃");
}
}
}
}
}.start();
}
}
我们可以看到程序还在运行当中,但是由于两个线程都在等待对方释放锁,所以就卡在这里。
它的过程其实是:第一个线程开始执行到 System.out.println(“获取”+s1+“等待”+s2); 这里时,cpu转向另外一个线程,这个线程执行到 System.out.println(“获取”+s2+“等待”+s1); 时,第一个线程在等待第二个线程释放s2,第二个线程在等待第一个线程释放s1,这样就出现了死锁的情况。