java多线程

什么是多线程

介绍多线程之前要介绍线程,介绍线程则离不开进程。

什么是进程?线程?

进程可以看做一个程序。

线程是一个程序的执行单元/一个场景。

进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。(每一个线程就是一个栈)

多线程就是一个进程运行时产生了多个线程。

线程对象的生命周期

  1. 新建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 死亡状态

创建线程

一.继承Thread类

步骤:①、定义类继承Thread;

     ②、复写Thread类中的run方法;

    目的:将自定义代码存储在run方法,让线程运行

     ③、调用线程的start方法:

    该方法有两步:启动线程,调用run方法。

public class Test3 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

public class Test4 {
    public static void main(String[] args) {
        Test3 test3 = new Test3();
        test3.start();
    }
}

注意.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)

t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

 

实现Runnable接口:

实现步骤: ①、定义类实现Runnable接

     ②、覆盖Runnable接口中的run方法,将线程要运行的代码放在该run方法中。

     ③、通过Thread类建立线程对象。

     ④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象。

     ⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

public class Test3 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

public class Test4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Test3());
        thread.start();
    }
}

3、通过Callable和Future创建线程:

实现步骤:①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。

                 ②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值

     ③、使用FutureTask对象作为Thread对象启动新线程。

     ④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

public class Test3 implements Callable {
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 0; i < 10; i++) {
           sum+=i;
        }
        return sum;
    }
}


public class Test4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Test3 test3 = new Test3();
        ExecutorService es = Executors.newFixedThreadPool(1);
       
        Future<Integer> future = es.submit(test3);
        
        Integer i = future.get();
        System.out.println(i);
    }
}

继承Thread类和实现Runnable接口、实现Callable接口的区别。

继承Thread

优点:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。

缺点:已经继承了Thread类,无法再继承其他类。

实现Runnable

优点:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

缺点:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

实现Callable:

优点:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

缺点:比较复杂、访问线程必须使用Thread.currentThread()方法

在实际开发过程中一般使用实现Runnable的方式创建多线程。

获取当前线程对象、获取线程对象名字、修改线程对象名字

方法名作用
static Thread currentThread()获取当前线程对象
String getName()获取线程对象名字
void setName(String name)修改线程对象名字

当线程没有设置名字的时候,默认的名字是什么?

  • Thread-0
  • Thread-1
  • Thread-2
  • Thread-3

关于线程的sleep方法

方法名作用
static void sleep(long millis)让当前线程休眠millis秒

静态方法:Thread.sleep(1000);
参数是毫秒
作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。

关于线程中断sleep()的方法

方法名作用
void interrupt()终止线程的睡眠

Java进程的优先级

方法名作用
int getPriority()获得线程优先级
void setPriority(int newPriority)设置线程优先级

线程安全

当多个线程访问是就可能会出现线程安全

一个多窗口买票:

public class MP implements Runnable{
    private static int a=100;
    private Object obj = new Object();
    @Override
    public void run() {
       while (true){
          
               if (a > 0) {
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "票号" + a);
                   a--;
               
           }
       }
    }
}


public class Test3 {
    public static void main(String[] args) {
        MP mp = new MP();
        Thread thread1 = new Thread(mp,"一号");
        Thread thread2 = new Thread(mp,"二号");
        Thread thread3 = new Thread(mp,"三号");
        Thread thread4 = new Thread(mp,"四号");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

 我们可以看到,这里出现了一些重复票,为什么会出现这种情况,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了a的值,刚把这个值读取出来还没有改变a的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的a值是一样的。

多线程安全问题解决

同步代码块

格式:


synchronized (锁对象) {

    可能会产生线程安全问题的代码
}

把上述代码加上synchronized后

public class MP implements Runnable{
    private static int a=100;
    private Object obj = new Object();
    @Override
    public void run() {
       while (true){
           synchronized (obj) {
               if (a > 0) {
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "票号" + a);
                   a--;
               }
           }
       }
    }
}

可以看到没有了重复票

提示:加上同步后只有一个线程在执行是CPU太好了,可以加大基数就可以看到多个线程。

JDK1.5后的同步解决

在JDK5之前,锁的获取和释放都是隐式的,看不见。也就说我们并没有直接看到在哪里加上了锁,在哪里释放了锁。到了JDK5的时候,java中提供了一个Lock接口,在这个接口中定义了锁的释放和获取的方法,后期程序中需要使用同步,这时可以使用Lock接口的实现类显示的完成锁的获取和释放的动作。

public void lock():加同步锁。

public void unlock():释放同步锁。

注意:在使用Lock锁对象时,需要手动的书写代码用来:获取锁、释放锁

 由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数。

用Lock改造上述买票后代码

public class MP implements Runnable{
    private static int a=100;
    private Object obj = new Object();
    Lock l=new ReentrantLock();
    @Override
    public void run() {
       while (true){
           l.lock();
               if (a > 0) {
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "票号" + a);
                   a--;
               }
           l.unlock();
       }
    }
}

同步的前提:

  1、必须要有两个或者两个以上的线程。

  2、必须是多个线程使用同一个锁。

  3、必须保证同步中只能有一个线程在运行。

  4、只能同步方法,不能同步变量和类。

  5、不必同步类中所有方法,类可以拥有同步和非同步的方法。

  6、如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

死锁

  进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

public class DeadLock {

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

class DeadLockTest implements Runnable {

    private boolean flag;
    static Object obj1 = new Object();
    static Object obj2 = new Object();

    public DeadLockTest(boolean flag) {
        this.flag = flag;
    }

    public void run() {
        if (flag) {
            synchronized (obj1) {
                System.out.println("if lock1");
                synchronized (obj2) {
                    System.out.println("if lock2");
                }
            }
        } else {
            synchronized (obj2) {
                System.out.println("else lock2");
                synchronized (obj1) {
                    System.out.println("else lock1");
                }
            }
        }
    }
}

 

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。

在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。

可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值