java多线程学习总结

    技术2025-03-29  20

    大纲:

    基本概念:程序、进程、线程线程的创建和使用线程的生命周期线程的同步线程的通信JDK5.0新增线程创建方式

    目录

    一、基本概念:

    二、线程的创建

    1、方法一:继承Thread类

    2、创建线程的方法二:实现Runnable接口

    Thead和runnable两种创建线程方式对比

    三、线程的方法:

    四、线程的生命周期:

    五、解决多线程安全问题:

    方式一:同步代码块

    方式二:同步方法

    方式三:JDK5.0新增方法:Lock锁

    synchronized与Lock的对比:

    六、线程之间的通信

    七、sleep和wait方法的异同:

    八、生产者和消费者问题:

    九、新增的创建多线程方式:

    1、实现Callable接口

    2、使用线程池方式:



    一、基本概念:

    1、程序:一段静态的代码

    2、进程:是程序的一次执行过程,或者是正在运行的一个程序,是一个动态的过程。有它自身的产生、存在、和消亡的过程——生命周期

    进程作为资源分配的单位,系统会在运行时为每个进程分配不同的内存区域

    3、线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。

    线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间的通信更便捷、高效。但多个线程操作共享的系统资源可能会带来安全隐患

    4、并发:多线程

    5、并行:多进程

    二、线程的创建

    1、方法一:继承Thread类

    创建一个类A继承Thread类重写Thread类中的run方法创建类A的一个实例对象通过此对象调用start()方法 public class ThreadTest06 { public static void main(String[] args) { NumberThread t1 = new NumberThread(); // start方法的作用:1.启动当前线程 2.调用当前线程的run()方法 t1.start(); // 问题:再启动一个线程,遍历100以内的偶数,不可以还让已经start()过的线程去执行,否则会报IllegalThreadStateException NumberThread t2 = new NumberThread(); t2.start(); t1.start(); for (int i = 0; i < 100; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + " " + i); } } } } class NumberThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + " " + i); } } } }

              使用匿名对象、子类的方式创建线程:

    public class ThreadTest06 { public static void main(String[] args) { new Thread(){ @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + " " + i); } } } }.start(); } }

    2、创建线程的方法二:实现Runnable接口

    创建一个实现了Runnable接口的类实现类去实现Runnable中的抽象方法:run()创建实现类的对象将此对象作为参数传递给Thread类的构造器中,创建Thread类的对象通过Thread类的对象调用start()方法 public class ThreadTest08 { public static void main(String[] args) { runThread r = new runThread(); Thread t = new Thread(r); t.start(); } } class runThread implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+" "+i); } } } }

    Thead和runnable两种创建线程方式对比

    继承Thread类

    public class ticketThread { public static void main(String[] args) { window1 t1 = new window1("窗口一"); window1 t2 = new window1("窗口二"); window1 t3 = new window1("窗口三"); t1.start(); t2.start(); t3.start(); } } class window1 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { System.out.println(getName() + ":" + ticket); ticket--; } else { break; } } } public window1(String name) { super(name); } }

    实现runnable接口:

    public class ticketRunnable { public static void main(String[] args) { win w = new win(); Thread t1 = new Thread(w, "窗口一"); Thread t2 = new Thread(w, "窗口二"); Thread t3 = new Thread(w, "窗口三"); t1.start(); t2.start(); t3.start(); } } class win implements Runnable{ private int ticket = 100; @Override public void run() { while (true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+" " +ticket); ticket--; }else { break; } } } }

    两种方式对比:

    开发中优选选择:实现Runnable接口的方式

    原因:

    1、实现接口的方式没有类的单继承性局限

    2、实现的方式更适合来处理多个线程共享数据的情况,例如上面两个代码的ticket。继承Thead的方式必须把ticket声明为static类型,而实现runnable的方式不需要,因为其天生就只有一份

    两种方式的联系:翻看Thread的源码就知道,其实Thread类也继承了Runnable接口。

    给线程命名方式一:使用setName()方法

    public class ThreadTest07 { public static void main(String[] args) { NameThread t1 = new NameThread(); t1.setName("线程一"); t1.start(); } } class NameThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+" "+i); } } } }

    线程命名方式二:重写构造方法

    public class ThreadTest07 { public static void main(String[] args) { NameThread t1 = new NameThread("线程一"); t1.start(); } } class NameThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+" "+i); } } } public NameThread(String name){ super(name); } }

     

    三、线程的方法:

    1、yield()方法:暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法。自己也会重新加入竞争

    2、join()方法:在线程a中调用b线程的join方法,此时线程a就进入阻塞状态,直到线程b执行完后,线程a才结束阻塞状态

    public class ThreadTest07 { public static void main(String[] args) { NameThread t1 = new NameThread("线程一"); t1.start(); for (int i = 0; i < 100; i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+" "+i); } if(i==2){ try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } class NameThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0) { System.out.println(Thread.currentThread().getName()+" "+i); } } } public NameThread(String name){ super(name); } }

    3、sleep(long millitime):让当前线程睡眠指定毫秒时长,在指定的millitime毫秒时间内,线程是阻塞状态

    4、isAlive() 判断当前线程是否存活

    线程的优先级

    四、线程的生命周期:

    用图总结吧

    五、解决多线程安全问题:

    方式一:同步代码块

    synchronized (同步监视器){ // 需要被同步的代码 }

    说明:

    同步监视器,俗称:锁。要求多个线程必须使用同一把锁如果是使用实现runnable的方式实现锁,则同步监视器可以是 this如果是使用继承thread类的方式实现锁,则同步监视器可以是同步代码块所在类的 class对象

    方式二:同步方法

    如果是使用实现runnable的方式实现锁,则同步监视器默认为this如果是使用继承thread类的方式实现锁,则同步方法必须使用static修饰,且其同步监视器默认是代码所在类的class。如果同步方法不用static修饰,则同步监视器默认为this

    单例模式的线程安全实现方式:

    class Dog{ private static Dog instance = null; private Dog(){ } public static Dog getInstance(){ if(instance ==null){ synchronized (Dog.class){ if(instance == null){ instance = new Dog(); } } } return instance; } }

    方式三:JDK5.0新增方法:Lock锁

    创建锁的时候可以传入一个boolean类型的参数,若是true则此锁为公平锁,否则为非公平锁公平锁的含义是在外面等待的线程在可以执行后按照先后顺序执行

    而synchronized只有非公平锁 

    public class ticketRunnable { public static void main(String[] args) { win w = new win(); Thread t1 = new Thread(w, "窗口一"); Thread t2 = new Thread(w, "窗口二"); Thread t3 = new Thread(w, "窗口三"); t1.start(); t2.start(); t3.start(); } } class win implements Runnable { private int ticket = 100; // 这里可以传入一个boolean类型的参数,若是true则此锁为公平锁,否则为非公平锁 // 公平锁的含义是在外面等待的线程在可以执行后按照先后顺序执行 private ReentrantLock lock = new ReentrantLock(true); @Override public void run() { while (true) { try { lock.lock(); if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + ticket); ticket--; } else { break; } } finally { lock.unlock(); } } } }

    lock还有 如下两种方法:

    tryLock(5, TimeUnit.SECONDS); lockInterruptibly();

    synchronized与Lock的对比:

    Lock是显示锁(手动开启和手动关闭,别忘记关闭锁),Synchronized是隐式锁,出了作用域自动释放Lock只有代码块锁,synchronized有代码块锁和方法锁使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且有更好的扩展性(提供更多的子类)ReentrantLock 有公平和非公平的区别

    优先使用顺序:Lock -> 同步代码块(已经进入了方法体,分配了相应资源)-> 同步方法(在方法体之外)

    1、syncronized最后会进入等待队列,不占用cpu。lock一般实现方式是cas(类似于自旋锁)会不停的轮询能不能拿到锁,会占用cpu

    2、什么时候用自旋锁什么时候用重量级锁:

    a. 线程少-自旋锁      线程多-重量级锁,即进入等待队列

    b. 操作消耗时间长-重量级锁

    六、线程之间的通信

    交替打印100以内的数字

    public class NumSkip { public static void main(String[] args) { Numb n = new Numb(); Thread t1 = new Thread(n,"打印机一:"); Thread t2 = new Thread(n,"打印机二:"); t1.start(); t2.start(); } } class Numb implements Runnable{ private int num = 1; @Override public void run() { while (true){ synchronized (this) { if (num <= 100) { notify(); try { Thread.sleep(100); }catch (Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + num); num++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } }

    涉及到三个方法:

    wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器notify(): 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级最高的那个notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程

    注意:

    三种方法只能使用在同步代码块中或者同步方法中,在Lock方式的线程中不能使用三种方法调用者必须是同步代码块或同步方法中的同步监视器,否则会报错wait()、notify()、notifyAll()三个方法都是在Object类中声明的,不是在Thread类中声明的。原因:因为其三个的调用则必须是同步方法或者同步代码块中的同步监视器,二同步监视器又可以是任务类型的,所以这三个方法不能声明在Thread类中,声明在Object类中,任何一个类都继承了Object类,也就拥有了这三个方法。

    七、sleep和wait方法的异同:

    相同点:都会使得线程进入阻塞状态

    不同点:

    sleep不会释放锁,而wait会释放锁sleep是声明在Thead类中,而wait是声明在Object类中sleep可以在任何场景下使用,而wait必须在同步代码块中使用sleep使用interrupt唤醒,而wait使用notify唤醒

    八、生产者和消费者问题:

    public class ProducerAndConsumerTest { public static void main(String[] args) { Clerks c = new Clerks(); Prod pro = new Prod(c); Cons con = new Cons(c); pro.start(); con.start(); } } class Clerks { private int count = 0; public synchronized void produceThing() { System.out.println(); if (count < 20) { count++; System.out.println(Thread.currentThread().getName() + "开始生产第" + count + "个产品"); notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consumeThing() { if (count > 0) { System.out.println(Thread.currentThread().getName() + "开始消费第" + count + "个产品"); count--; notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Prod extends Thread { private Clerks clerk; public Prod(Clerks clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(getName() + "开始生产产品"); while (true) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceThing(); } } } class Cons extends Thread { private Clerks clerks; public Cons(Clerks clerks) { this.clerks = clerks; } @Override public void run() { System.out.println(getName() + "开始消费产品"); while (true) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerks.consumeThing(); } } }

    九、新增的创建多线程方式:

    1、实现Callable接口

    创建一个实现Callable的实现类重写call方法,将此线程需要执行的操作声明在call中创建Callable接口实现类的对象将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并start() public class CallTest { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask futureTask = new FutureTask(numThread); new Thread(futureTask).start(); try { // get()返回值即为FutureTask构造器Callable实现类重写call()的返回值 Object sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } // class NumThread implements Callable { @Override public Object call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i); sum += i; } } return sum; } }

    2、使用线程池方式:

    public class ThredPoolTest { public static void main(String[] args) { // 下面这行代码获取的是一个接口的类型(多态) ExecutorService service1 = Executors.newFixedThreadPool(3); // 只有将其转换为具体的类才可以对其设置属性 ThreadPoolExecutor service = (ThreadPoolExecutor)service1; service.setCorePoolSize(6); service.execute(new NumT()); // 用完关闭线程池 service.shutdown(); } } class NumT implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i); } } }

    十、释放锁和不释放锁的操作总结

    volatile

    volatile有两个特性:

    保证线程可见性 — MESI— 缓存一致性协议 禁止指令重排序

    DCL单例 public class Singleton2 { private static volatile Singleton2 INSTANCE; private Singleton2() { } public static Singleton2 getInstance() { if (INSTANCE == null) { synchronized (Singleton2.class) { if(INSTANCE==null) { try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } INSTANCE = new Singleton2(); } } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 30; i++) { new Thread(new Runnable() { public void run() { System.out.println(Singleton2.getInstance().hashCode()); } }).start(); } } }

    volatile不能保证原子性,只能保证可见性

    synchronized优化

    1、锁的粒度缩小

    2、锁定某个对象o,如果o的属性发生改变,不影响使用,但是如果O变为另外一个对象,则锁定的对象发生改变。应避免将锁定对象的引用编程另外的对象

    public class ChangeLock { Object o = new Object(); void m(){ synchronized (o){ while (true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } } } public static void main(String[] args) { ChangeLock c = new ChangeLock(); new Thread(c::m,"t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(c::m,"t2"); // 说对象发生改变,所以t2线程得意执行,如果注释这句话,线程2永远得不到执行机会 // c.o = new Object(); t2.start(); } }

    CAS:CompareAndSet  

    也有人叫其乐观锁。cas在unsafe类中,可以直接操作内存

    Atomic 

    凡是Atomic开头的都是通过cas保证线程安全

    public class AtomicTest { AtomicInteger count = new AtomicInteger(0); void m(){ for (int i = 0; i < 1000; i++) { count.incrementAndGet(); } } public static void main(String[] args) { AtomicTest t = new AtomicTest(); ArrayList<Thread> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(new Thread(t::m,"thread-"+i)); } list.forEach((o)->{o.start();}); list.forEach((o)-> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }

    ABA问题:

    如果要操作的为基本数据类型或者数值类型没问题,如果是引用类型则可能会发生问题。解决方法是加版本号

    三种方式的对比:

    public class CompareThread { static long count2 = 0L; static AtomicLong count1 = new AtomicLong(0L); static LongAdder count3 = new LongAdder(); public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int i1 = 0; i1 < 100000; i1++) { count1.incrementAndGet(); } }); } long start = System.currentTimeMillis(); for (Thread thread : threads) thread.start(); for (Thread thread : threads) thread.join(); long end = System.currentTimeMillis(); System.out.println("count1-Atomic "+count1+" time: "+(end-start)); //------ Object o = new Object(); for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int i1 = 0; i1 < 100000; i1++) { synchronized (o) { count2++; } } }); } start = System.currentTimeMillis(); for (Thread thread : threads) thread.start(); for (Thread thread : threads) thread.join(); end = System.currentTimeMillis(); System.out.println("count2 "+count2+" time: "+(end-start)); // -------- for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(()->{ for (int i1 = 0; i1 < 100000; i1++) { count3.increment(); } }); } start = System.currentTimeMillis(); for (Thread thread : threads) thread.start(); for (Thread thread : threads) thread.join(); end = System.currentTimeMillis(); System.out.println("count3-LongAdder "+count3+" time: "+(end-start)); } }

    结果对比:

    为什么Atomic比synchronized要快,因为synchronized加锁了,有可能会去操作系统申请重量级锁。而Atomic内部使用了cas操作

    LongAder为什么比Atomic还要快呢,是因为其内部使用了分段锁

     

     

    Processed: 0.009, SQL: 9