欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程的基本操作

程序员文章站 2022-05-04 18:17:19
...

使用多线程来代替多进程进行并发程序的设计,是因为线程的切换和调度的成本要远远小于进程。

线程的基本操作

1.1 新建线程

Thead t1 = new Thread();
t1.start();

线程start()后会新建一个线程并让这个线程执行run()方法。

1.2 终止线程

stop()的问题:

stop()方法会强制终止一个线程,这可能会导致数据不一致的问题,这样做是不安全的。

可以用一个标记变量,用于指示线程是否需要退出。

1.3 线程中断

严格讲,线程中断并不会线程立即退出,而是给线程发送一个通知,告知目标线程希望它退出。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。

与线程中断有关的三个方法:

public void Thread.interrupt(); //中断线程
public boolean Thread.isInterrupted();  //判断是否被中断
public static boolean Thread.interrupted();  //判断是否被中断,并清除当前中断状态

Thread.interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。Thread.isInterrupted()方法也是一个实例方法,它通过检查中断标志位判断当前线程是否被中断。

中断线程时需要在run()中写有中断处理代码。

Thread t1 = new Thread(){
    @Override
    public void run() {
        while (true) {
            //中断处理
            if (Thread.currentThread().isInterrupted()) {
                	System.out.println("Interrupted!");
                	break;
            }
            
            Thread.yield();
        }
    }
}

注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位。

package interrupt;

import javax.swing.*;

public class InterruptedTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("interrupted!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted When sleep");
                        //设置中断状态
                        Thread.currentThread().interrupt();
                    }
                    Thread.yield();
                }
            }
        };

        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}

1.4 等待(wait)和 通知(notify)

这两个方法在Object类中,当一个对象实例调用了wait()方法后,当前线程就会在这个对象上等待。线程被notify唤醒后,需要获得这个对象的锁才可以继续执行后续代码,而不是立即执行。

public class SimpleWN {
    final static Object object = new Object();
    public static class T1 extends Thread {
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ":T1 start!");

                try {
                    System.out.println(System.currentTimeMillis() + ":T1 wait for object");
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ":T1 end!");
            }
        }
    }

    public static class T2 extends Thread {
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ":T2 start!notify one thread");
                object.notify();
                System.out.println(System.currentTimeMillis() + ":T2 end!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new T1();
        Thread t2 = new T2();
        t1.start();
        t2.start();
    }
}

运行结果:
线程的基本操作

T1在延迟了两秒后才执行T1 end! 说明T1在被唤醒后仍然在等待获取object的锁,获取之后才执行后续代码。

注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

1.5 挂起(suspend)和继续执行(resume)线程

这两个操作时一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续执行。但这两个方法不推荐使用。

因为suspend()暂停线程时不会释放任何锁资源,如果resume()意外的在其之前执行了,那么该线程就会被永久挂起。

1.6 等待线程结束(join)和谦让(yield)

JDK中有两个join方法:

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。

yield()方法:

public static native void yield();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。如果觉得一个线程不是那么重要,或者优先级比较低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用yield()方法,给予其他重要线程更多的工作机会。

sleep 和 yield的区别:

sleep()会让线程从运行态进入到阻塞态,不会被分配CPU时间片,而yield()方法可以主动让出CPU,它使线程从运行态进入到就绪状态,仍然有机会被处理机调度。

初识volatile

当使用volatile关键字声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。

package volatileTest;

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                System.out.println(number);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(10000);
    }
}

线程的基本操作

给ready加了volatile关键字后,告诉Java虚拟机,这个变量可能会在不同的线程中修改:

线程的基本操作

线程安全和关键字synchronized

1、线程安全

线程安全是并行程序的根基。volatile关键字并不能真正确保线程安全,它只能确保一个线程修改了数据后,其他线程能看到这个改动,但当两个线程同时修改某一个数据时,依然会产生冲突。

2、关键字synchronized

关键字synchronized的作用是实现线程间的同步。

它的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性。

关键字synchronized有多种用法:

  • 指定加锁对象:对给定对象加锁,进入同步代码块前需要获得给定对象的锁
  • 直接作用于实例方法:相当于对当前实例方法加锁,进入同步代码块之前要获得当前实例的锁
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码块之前要获得当前类的锁。

关键字synchronized还可以保证线程间的可见性和有序性,从可见性角度上讲,关键字synchronized可以完全替代关键字volatile的功能,但使用上没有那么方便。就有序性而言,由于关键字synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语句一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁之后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,有序性问题自然得到了解决。

多线程下的ArrayList和HashMap

1、ArrayList

由于容器ArrayList不是线程同步的,所以如果多个线程同时访问一个ArrayList,就会出现数据的冲突。

解决办法是用Vector代替ArrayList

2、HashMap

HashMap也不是线程安全的,在JDK8之前,如果多个线程同时向HashMap中添加数据,则可能会造成死循环,这是因为破坏了HashMap内部的结构,将链表变成了环,即两个节点互相指向对方,但在JDK8之后解决了这个问题,但仍然不能再多线程环境下使用HashMap,可以用ConcurrentHashMap代替。

错误的加锁

static Integer i = 0;

increase() {i++}

synchronized(i) {

increase();

}

这样的锁,仍然不会使得线程安全,因为把锁加在了变量i上,而Integer属于不变对象,即对象一旦创建,就不可能被修改。也就是说,如果你有一个Integer对象代表1,那么它就永远代表1,你不可能修改Integer对象的值,使它为2,如果需要2,则必须重新创建一个Integer对象,并让它来表示2。

反编译后会发现i++实际上是在执行i =Integer.valueOf(i.intValue() + 1);,Integer.valueOf()是一个工厂方法,它会倾向于返回一个代表指定数值的Integer对象实例。因此i++的本质是创建一个新的Integer对象,并将它的引用赋值给i。

由于在多个线程间,并不一定能够看到同一个i对象(因为i对象在不断地改变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码块控制出现问题。