线程实现的三种方法
1. 继承Thread
这种方法是继承,接下来实现run方法,在方法下写上作为线程被调用的时候需要运行的代码。
注意下面的代码,调用MyThread成为线程,并且start()以后,只会运行run里面的代码。除非你直接运行MyThread,不然main方法是不会运行的。
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++)
System.out.println(i);
}
public static void main(String[] args) {
System.out.println(123);
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
run方法描述的就是你这个线程在运行的时候要去做的事,而不是说这个路径的触发方式。所以是thread.start(),不是thread.run()。所以它们的关系是使用start()方法之后,这个thread会去跑run()里面的代码。
下面列一张图,大致描述两个线程运行的过程。进程结束的一条原因就是它的所有线程全部结束了。
注意,在一个线程里面所执行的方法是跑在这个线程自己的栈里的。我们之前提到,每个线程都是有自己的独立栈空间,而方法又是加载在栈里的,这么说也就不难理解了。
2. 实现接口Runnable
Runnable的实现更偏向于是创建任务,而不是直接创建线程。先来看一下代码。这里同样的,MyThread在start之后也不会运行main函数。
public class MyThread implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10; i++)
System.out.println(i);
}
public static void main(String[] args) {
System.out.println(123);
}
}
public class Demo1 {
public static void main(String[] args) {
// 创建MyThread任务m
MyThread m = new MyThread();
// 创建线程t1,t2,并且都传入任务m
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
//线程运行
t1.start();
t2.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
这里主要的区别是,先new了一个MyThread对象,然后把这个对象传到Thread t里面,再让t.start()。这里的好处之一是让程序更健壮,因为创建线程和执行任务是分离的。
注意一下,这里t1和t2用的都是拿m这个对象,因此他们操作的都是同一块内存。所以这时候就可能会涉及到多线程安全性问题了。这个我们后面说。
总结一下实现Runnable与继承Thread相比有如下优势:
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况。比如都执行这个循环,我写一遍MyThread就好了,然后创建多个Thread对象,传入这个任务即可,不需要实现多个Thread类。
2. 可以避免单继承所带来的局限性。因为Thread继承以后就不能再继承其他类了,但是Runnable是接口,还可以允许继承其他类。
3. 任务与线程是分离的,提高了程序的健壮性
4. 线程池技术接受Runnable和Callable类型的任务,不接受Thread类型的线程
3. Callable
Callable 是JDK1.5之后出现的(Runnable是1.1),算是Runnable的进一步升级。它也是被视作任务,但是它的核心是call()方法,而不是run()方法,并且允许返回值以及异常处理。这些都是Runnable和Thread不允许的。看一下代码
//返回String类型
//注意这里implements Callable<T>
class CallDemo implements Callable<String>{
@Override
public String call() throws Exception {
return null;
}
}
class CallDemo implements Callable<T>{
@Override
public T call() throws Exception {
return null;
}
}
//申明方式
Callable<T> c = new Callable<T>(); //1
FutureTask<T> task = new FutureTask<T>(c); //2
Thread t = new Thread(task); //3
这里提一嘴他很好用的一个方法,就是get(),获取返回值。
如果使用了这个,那么相当于一个阻断,在这个之前的线程必须先运行完,下面的线程才能运行。
比如:
int i = t.get(); // 4 假设123里的T是Integer
for(...) //5
那么这里4必须要执行完才会去执行5
Thread类
1.常用构造方法有:
- Thread()
- Thread(String name)
- Thread(Runnable/ Callable demo)
- Thread(Runnable/ Callable demo, String name) //为线程命名
常用方法:
-
start() //启动线程
-
sleep(long millis) //让线程沉睡指定毫秒
-
getName() //获取线程名称,也可以setName()
-
getId() //获取线程id
-
getPriority() //获取此线程优先级
-
setPriority() //设置优先级 MAX_PRIORITY > NORM_PRIORITY > MIN_PRIORITY
-
getState() //获取状态
-
getThreadGroup() //查看位于哪一个线程池
-
Thread.yield() //运行状态下的线程 调用Thread.yield()进入就绪状态后,和其它就绪状态线程处于同一起跑线,也有可能被立即再次被调用;
-
wait() // 这个方法是让当前synchronized释放上锁的object。调用方法object.wait()
-
notify() // 这个方法是唤醒下一个等待的资源的线程。调用方法obj.notify()
-
notifyAll() // 这个方法是唤醒所有等待该资源的线程。调用方法obj.notifyAll()
当程序不明确知道下一个要唤醒的线程时,需要采用notifyAll()唤醒所有在wait池中的线程,让它们竞争而获取资源的执行权,但使用notifyAll()时,会出现死锁的风险,因此,如果程序中明确知道下一个要唤醒的线程时,尽可能使用notify()而非notifyAll()。 -
interrupt() //这种方法是比较安全的终止线程的方法。现在的java让线程能够安全结束的方法就是给线程上标记。线程在运行的时候在特定的情况下会去检查标记,一般在sleep的时候或者wait的时候(这两个会抛出InterruptedException)。因为使用这两个方法必须try catch,而有interrupt标记的时候会让他们进入catch块。这时候可以让他们安全关闭释放资源,也可以简单做其他处理,比如输出等。
这里注意一下,一般来说有三种中断线程的方法,有用一个变量中断,还有interrupt比起interrupt(),stop()方法和suspend()方法已经过时。原因是stop会让当前运行线程强制停止,如果这个线程尚未释放资源,强制停止等于把资源锁死。比如当前线程操作IO,直接stop会一直占用IO资源。而suspend则是容易造成死锁。
下面看一下interrupt的代码:
public class Demo5 {
public static void main(String[] args) {
//线程中断
//y一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给线程t1添加中断标记
t1.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
//线程自杀就直接return,这样run方法就结束了。如果不return程序会直接继续走,走完这个for循环。
return;
}
}
}
}
}
2. Synchronized浅析(隐式锁)
首先Synchronized只有两个锁的写法,一种是用锁代码块的写法,一种是直接锁方法的写法。而类锁一般对应代码块里直接synchronized(*.class)或者申明静态synchronized方法
1.修饰语句块的时候
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象 头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏ monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞 等待,直到锁被另外⼀个线程释放为⽌。
2.修饰方法
由于是修饰方法,因此并不需要monitorenter和exit。取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问 标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。
代码写法
1、修饰代码块(就是在代码块的花括号前面写上synchronized())
2、修饰指定对象的代码块
此时此刻,当一个线程访问a的对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问account对象结束
这里加一段比较有意思的问题,这段代码里,如果我在run()的方法内部申明Object的话,就无法完成同步。是因为当t1 t2他们start()以后,都会各自调用 run方法,而run方法每次调用都会新创建一个Object。这样子就相当于每个人都有自己的一把锁,当然无法锁住。这里两种方法:一种是改成this,一种是把Object放在run外面申明。因为放在run外面的话Object只有一次实例化,就是Runnable r = new Ticket();三把锁都对应这个Object。
public class MyThread {
public static void main(String[] args) {
Runnable r = new Ticket();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}
}
}
}
}
3、修饰指定类的代码块
4、修饰静态方法和动态方法的代码块:
public (static) synchronized void method()
都是public synchronized …
但是静态方法因为在一开始就会被加载到内存,所以静态方法代码块就相当于给类上锁
而动态方法其实就是synchronized(this),锁定新创建的对象。跟之前的this代码块很像,但是底层方法还是有一点差别
3. Reentranslock浅析(显式锁)
这是lock的子类。这个锁跟synchronized在代码上的区别就是,sync锁定和解锁由方法块自定义。而lock需要自己在语句上定义lock.lock(),解锁就是lock.unlock()
4. 公平锁和非公平锁 以及 可重入锁
5. 线程的六种状态
- New, 尚未启动的新线程,相当于在new Thread()这步
- Runnable,在JVM中运行的线程
- Blocked,被阻塞等待Monitor锁定的线程(这里涉及到sync底层机制,有兴趣的可以去搜一下,不然就简单理解为在等待解锁的排队中)
- Waiting,无限期等待另一个线程的操作中的状态,需要被唤醒
- Timed_waiting,计时等待,到时苏醒。
4和5唤醒以后理想情况都会进入runnable状态,也有可能被阻塞变成blocked - Terminated,线程结束状态
6. 生产者和消费者问题(著名的线程唤醒和沉睡问题)
这个代码的目的是让cook做100次菜,服务员取100次菜,并且按照顺序打印,一次老干妈香辣,一次煎饼果子甜辣…
先上一下正确的代码:
public class Demo12 {
public static void main(String[] args) {
//多线程通信 生产者与消费者问题
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
f.setNameAndTaste("老干妈小米粥","香辣味");
}else {
f.setNameAndTaste("煎饼果子","甜辣味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
接下来说一下我遇到的问题,与过程的演变。
下面的代码是最初始的代码,没有锁也没有其他控制机制。所以会有问题。这个问题是什么呢?
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public void setNameAndTaste(String name,String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void get(){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
}
}
就是比如我cook在setName的时候,waiter进来了。因为setName和setTaste并不是原子操作,所以可能cook刚设置完name,waiter就调用get打印了。所以food这个对象的name更新了,但是口味还没更新,所以就会打印出”老干妈小米粥,甜辣味“。
于是Food类更新,出了2.0。那直接给两个方法上锁岂不就好了?但是这样一样会有问题。问题是什么呢?
问题就是虽然上锁了,但是因为java是优先调度资源,会抢占资源。比如cook拿到了锁,它结束以后比waiter抢占资源快,就会一直调用,那么就会导致cook先循环完100次,那么food就固定是煎饼果子甜辣了,而waiter后面运行通过get输出的就全是这个了。
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public synchronized void get(){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
}
}
那么到底该如何解决?首先sync不变,但是定义一个boolean变量flag。这个flag是true的时候说明可以生产,false的时候说明可以端菜。这样子我在生产以后把flag变成false,那就算cook再抢到资源进来发现是false也不能继续做菜了,只能等waiter进来get以后,把flag置换成true才能继续做菜。同时这里很巧妙的运用了一个wait()。这是因为就算cook进来发现flag是false可是他还是能一直进来抢占资源,这样会导致waiter很久以后才能拿到锁执行。可是如果加了一个wait(),那么这个线程在调用一次方法后就会沉睡,需要等其他线程拿到锁以后将他们唤醒。这里的this.notifyAll()指的是我把调用这个对象的所有线程全部唤醒。注意这里food是对象而不是线程,其他两个才是线程。
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}