Java多线程编程

线程的基本使用

 

1.继承Thread

public class ThreadTest2 extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"=>实现run方法");
    }

    public static void main(String[] args) {
        ThreadTest2 myThread=new ThreadTest2();
        myThread.start();
        
    }
}

2.实现runable接口

public class RunableImpl implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "=>实现run方法");
    }

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

3.使用线程池

import java.util.concurrent.*;

public class CallableTest implements Callable<String>{

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //创建一个有10个线程的线程调度服务
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 100; i++) {
            //提交100个任务,让调度服务执行
            scheduledExecutorService.submit(new CallableTest());
        }
    }

    @Override
    public String call(){
        System.out.println(Thread.currentThread().getName()+"执行call()");
        //call方法可以有返回值,这个在线程池中再详细说
        return "";
    }

}

线程的生命周期

 

线程常用方法

方法签名说明备注
synchronized void start()启动线程 start方法只能被调用一次,也就是说一个线程只能被启动一次。如果start被调用1次以上则会抛出异常IllegalThreadStateException
void run()线程启动成功后会自动调用run方法

Thread类默认实现了Runable接口,如果创建线程时没有指定Runable对象或者Thread子类没有重写run方法,则会执行Thread类实现的run方法,该方法将不执行任何动作                                          

void sleep(long millis) 

 

void sleep(long millis, int nanos)

线程休眠指定毫秒

线程休眠毫秒+纳秒 

线程休眠后进入阻塞状态,休眠不会释放锁
void suspend()暂停这个线程

suspend方法已经不推荐使用,因为suspend容易导致线程死锁。

简单分析一下 suspend和sleep的区别

虽然suspend和sleep都可以导致线程等待,而且两个方法都不会释放已经持有的监视器(锁),但是为什么suspend更容易导致死锁的发生呢?

suspend方法可以在线程外部调用,无论当前线程执行到什么代码都会被叫停,这就引发了区别,因为sleep只能暂停当前线程,也就是说只能在线程内部显示的暂停,这个时候线程持有哪些对象锁是明确知道的。

但是如果调用suspend那么线程对于我们来说就是一个黑匣子,它运行到哪里,当前持有什么对象锁,这些锁在后面的代码中是否需要都不知道,这就很可能在一些关键资源上被一直持有不能释放从而导致死锁。

如最常见的 System.out.println()方法 这个方法就是被synchronize修饰的,如果线程在执行 System.out.println()时被调用suspend暂停,后续的代码中如果在调用 System.out.println()就会进入死锁。

void resume()恢复挂起的线程由于suspend方法不安全所以对应的resume方法也不在推荐使用
void wait()

暂停线程,释放当前线程持有的锁

wait/notify 这对方法总是同步出现用来实现线程之间的协作与通信。使用注意事项

1、这两个方法只能在synchronized作用域内调用才有效,在没有获取到对象锁的地方调用会抛出异常IllegalMonitorStateException

2、一个监视器(锁)只能唤醒它这个监视器本身挂起的线程,不能唤醒其他监视器挂起的线程

3、为什么要设计成先获取锁才能去调用wait与notify方法

   个人理解:

1、方便通过相同的监视器协同相关的线程进行工作,比如notifyAll方法不会唤醒所有等待的线程,而是唤醒被同监视器挂起的线程

2、因为wati与notify在同步代码块中从而避免了同时接收到指令而引起的并发问题,如果没有这个限制一个线程被另外两个线程分别调用wait和notify方法就会引起并发问题

native void notify()唤醒一个被wait方法等待的线程,当前线程不会立刻释放对象锁
native void yield()放弃当前cpu资源线程放弃cpu资源后,可能马上又获得cpu资源
void join()等待线程对象销毁

join方法中有一个while循环,改循环只有在线程死亡后(died)才会结束,也是因为这一点join方法的调用会导致调用者进入等待状态,其实就是join方法一直没有执行完。利用这个特点可以在一些场景中等待线程执行完成,在执行其他操作。但是通过对源码的分析可以看到,join方法会一直获取锁释放锁,这样的方法是比较耗性能的。

void stop()强制线程停止执行。

stop方法已经被标记为不推荐使用,引用API中的说明:

使用stop停止线程。会导致它解锁所有已锁定的监视器(这是未检查的ThreadDeath异常向上传播堆栈的自然结果)。如果以前受这些监视器保护的任何对象处于不一致的状态,则被损坏的对象对其他线程可见,可能导致任意行为。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

wati 与notify使用方式


public class WaitNotifyTest extends Thread {

    private Object lock = new Object();//定义对象锁

    private volatile int threadSwitch = 1;//控制等待的开关

    private String id;//线程id

    public WaitNotifyTest(String id) {  this.id = id; }

    @Override
    public void run() {
        while (true) {
            synchronized (lock) { //先获取锁在调用wait()
                try {
                    Thread.sleep(100);
                    System.out.println("run id=" + id);
                    if (threadSwitch == 2) {  lock.wait();  }
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }

    public void off() {  this.threadSwitch = 2;  }

    public void on() {
        synchronized (lock) { //先获取锁在调用notifyAll
            this.threadSwitch = 1;
            lock.notifyAll();
            System.out.println("唤醒id=" + id);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyTest t1 = new WaitNotifyTest("1");
        WaitNotifyTest t2 = new WaitNotifyTest("2");
        //启动两个线程
        t1.start();
        t2.start();
        Thread.sleep(1000);
        //使线程进入等待状态
        t1.off();
        t2.off();
        Thread.sleep(1000);
        //唤醒线程1
        t1.on();
    }
}

输出结果:

run id=2
run id=1
run id=2
run id=1
run id=1
run id=2
run id=2
run id=1
run id=1
run id=2
run id=1
run id=2
run id=1
run id=2
run id=1
run id=2
run id=2
run id=1
run id=1
run id=2
唤醒id=1    // 从后面的输出结果可以看出,调用notifyAll()只唤醒了线程1,线程2继续处于等待状态
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1
run id=1

java线程与操作系统内核线程的关系

  java中的线程是直接映射到操作系统中的线程的,我们来验证一下,我们在代码中创建1000个线程,然后看操作系统中的线程数量的变化

public class ThreadTest1 {
    public static  void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"创建");
                    try {
                        Thread.sleep(1000*10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        Thread.sleep(1000*20);
    }
}

在windows系统中运行之前线程数为3909

运行代码后 

由此可以说明虚拟机是直接在内核中创建了线程,我们在linux系统中在测试一下

在linux系统 中运行 

查看进程id

找到进程id为27418

在通过top命令查看这个进程的详细信息

同top命令可以看到27418进程中有1020个线程,说明我们创建的1000个线程也映射到了操作系统内核中。

通过查看Thread类的源码可以看到start方法中通过调用一个被 native 修饰的start0方法启动线程,这也说明了启动线程是会调用操作系统本地方法的。

总结:由于java对于线程的启动,销毁,需要调用系统内核资源,这个过程是需要耗费很多资源的,所以在程序中不能无限制的去创建线程,最好使用可控的线程池来完成任务的调度。

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值