java基础之多线程的魅力

254 阅读6分钟

这两天学习多线程的基础(趁热总结一下)

一 多线程的概念:

1.1 什么是多线程?

在说线程之前不得不提的并发和并行的概念:

1.1.1并发和并行的概念:

  • 并发:cpu在执行任务的时候是在同一时间段内cup来回执行这两个事件,也就是交替执行,因为速度很快,所以我们感觉是共同执行的。
  • 并行:cpu在同一时间段内同时执行两个事件。注意是:同时执行。

1.1.2 线程和进程的概念:

  • 进程的概念:

多为内存中运行的应用程序,而每个程序他是占用内存空间的,是需要在内存中运,一个程序可以是一个进程,也可以是多个进程,而一个进程包含一个线程叫单线程,包含多个线程叫多线程。运行程序的过程也是进程运行的过程。

  • 线程的概念:

(首先我感觉在此之前肯定是多进程的东西,但是为什么会有多线程呢,因为凡是计算机,涉及到内存的东西,肯定是进行优化提高的,虽然在现在的电脑配置体现不大,肯在过去的程序编写的时候肯定要考虑程序的所需要的内存,一个程序的核心也就是算法,而算法是需要时间和空间的复杂度,相比之下,为了提高算法的效率,肯定是要优化程序 因为线程是需要占用内存的而频繁的占用内存是不可取的,所引出现了线程,线程是不用占内存空间的,线程是进程的儿子吧。)以上是我猜的

假如一个进程是一个应用程序,而这个应用程序可能有多个功能要求执行,而这些功能也就是线程的体现,线程没有自己独立的内存空间,他们利用进程共享的资源。进程作为程序分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位一个线程的崩塌会导致一个进程的销毁,可能这就是他们说的线程不安全的地方,所以多进程的程序?会比多线程显得健壮。

1.1.3 一个线程的崩塌

(如果你和我一样是初学者,建议你最后再看)

  • 代码如下:

       public static void main(String[] args) {
      //创建lock锁(多太)
      Lock lock = new ReentrantLock(true);//公平锁
      //利用匿名类对象创建线程
      Runnable testRun = new Runnable() {
          @Override
          public void run() {
              lock.lock();
              int[] arr = new int[3];
              System.out.println(arr[3]);
              lock.unlock();
          }
      };
      //创建Thread类并行执行线程
      Thread thread = new Thread(testRun);
      thread.start();
    
      //创建第二个线程
      new Thread(new Runnable() {
          @Override
          public void run() {
              lock.lock();
              System.out.println("我是第二个线程");
              lock.unlock();
          }
      }).start();
    

    }

运行结果:

    Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
	at threadcollapse.demo01$1.run(demo01.java:19)
	at java.base/java.lang.Thread.run(Thread.java:844)

运行分析 根据结果我们可以看到第二个线程并没有执行,因为在第一个线程获得锁之后,然后程序发生了数组越界异常,而异常并没有捕捉,导致线程异常退出。重点是出现异常的线程一直占用lock这个锁,并且在获得锁之后没有释放前就抛出了异常,那么这个锁就永远在占用,那么第二个线程试图获得这个锁的时候,以为上一个没有释放,他就一直获取不到就一直阻塞。

1.2 如创建一个线程呢?

方法1: 声明一个类是Thread类的子类,应该重写Thread类的run方法,设置线程任务。

代码入下:

 * 
 * 运行过程:两个线程并发的运行,(java的程序属于抢占性内存调度,那个线程的优先 级高,就执行那个线程,
 * 如果是同一个线程,在没有设置线程的优先级的时候,默认都是5,然后两个线程线程同时抢占cpu的资源)
 *当调用start方法的时候,两个线程(当前线程main主线程)和新创建的线程 并发的运行
 * 
 */
public class demo02 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//调用该方法的start方法会开启新的线程,并执行线程的run方法
        for (int i = 0; i <20 ; i++) {
            System.out.println("爸爸程执行次序"+i);
        }
    }
}

    public class MyThread extends Thread {//创建一个类继承了Thread类
    //重写里面的run方法,并且设置线程任务。
    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            System.out.println("儿子执行次数"+i);
        }
    }
}
运行结果:
(可以看出是两个线程交替运行的)


创建一个类,并且继承Thread类,然后重写里面run方法,(而run方法里面实现线程的具体操作),之后创建一个实现类对象,然后调用了里面start方法,执行线程。要注意的是:线程的start方法只能运行一次,多次调用线程的start方法会报错,当调用start方法的时候,会把线程添加到线程组中,等待线程调度器调用,当获取资源的时候,就会进入运行状态

1.2多线程的内存图

(根据上面的代码相应的内存图)

图片如下:

二 两种创建线程的方法(Thread类,Runnable接口)

2.1 thread类的一些常用的方法:

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行,Java虚拟机调用此线程的run方法
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停。
  • Thread currentThread() :返回对当前正在执行的线程对象的引用。
  • void setName(String name) 改变该线程的名字等于参数

代码如下:

2.1.1 获取线程名:

    //getName方法
 public static void main(String[] args) {
    myThread myThread = new myThread();
    myThread.start();
 }
 public class myThread  extends Thread{
    @Override
    public void run() {
        String name = getName();//通过getname获得线程的名称,返回值是一个string类型
        System.out.println(name);
    }
    }
    代码运行效果:
    Thread-0
    //直接调用获取线程名很好理解。
    
    *******************华丽的分割线************************
    
    //通过currenthread() 获得当前正在执行的线程。
    代码如下:
    public static void main(String[] args) {
        myThread myThread = new myThread();
        myThread.start();
        System.out.println(Thread.currentThread().getName());//我这里直接写一起了
   }
   
    public void run() {
        Thread thread = Thread.currentThread();//通过调用currenthread方法,返回一个thread对象
        String name = thread.getName();//然后再调用.getname方法 
        System.out.println(name);
     }
    //这两种方法都就可以获得线程的名称。
    运行效果:
    main
    Thread-0

2.1.2 设置线程的名称

两种方式修改线程的名称
1.获得main方法创建Thread的实现类之后,然后通过实现类.setName"")的形式设置线程的名称。
代码如下:
 myThread myThread = new myThread();
    myThread.setName("张博阳");
    myThread.start();
    
2.利用构造方法,创建Thread实现类的构造方法,直接super调用父类的方法,利用父类的方式在创建线程的时候直接起名。

代码如下:
public class myThread  extends Thread{

public myThread() {
}

public myThread(String name) {
    super(name);//直接让父类进行处理
}
}

public static void main(String[] args) {
 myThread threadName= new myThread("海绵宝宝");//在创建的时候直接传递参数
    threadName.start();
}

2.1.3 sleep方法

sleep(long millis) ,当前正在执行的线程休眠(暂停执行)为指定的毫秒数,根据精度和系统定时器和调度的准确性。 什么意思呢,方法的功能是让线程等待几秒钟。

代码如下:

    public class myThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 60; i >0; i--) {
            try {
                Thread.sleep(1000);//因为是继承Thread,父类没有抛出异常,这里也不能抛,只能处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("毫秒倒计时"+i+"秒");
        }
    }
  }
  
  
   public static void main(String[] args) {
    myThread2 myThread2 = new myThread2();
    myThread2.start();
 }
 运行效果:
    毫秒倒计时60秒
    毫秒倒计时59秒
    毫秒倒计时58秒
    毫秒倒计时57秒
    .....
    该方法让线程一秒钟休眠一次,实现了一个毫秒倒计时的功能。
    

2.2 Runable接口(创建线程的第二种方式)

2.2.1 Runable接口的步骤:

  • 定义Runnable 接口的实现类,然后重写里面的run方法,同Thread的run方法相同,也是设置线程任务。
  • 创建Runnable的实现类,(因为runnable接口中没有start方法)需要我们创建一个Thread类然后调用start方法开启线程.在Thread类的构造方法里面可以传递runnable 例如:thread(Runnable target) 分配一个新的 Thread对象。
  • 开启多线程。
  • 创建一个runnable接口的实现类
  • 重写里面的run方法,设置线程任务
  • 创建一个runnbale接口实现类对象
  • 创建一个Thread类对象,构造方法中传递Runnable对象参数
  • 执行Thread对象的start方法,开启多线程

代码如下:

//创建一个Runable接口的实现类
public class RunnbaleImpl implements Runnable {
@Override
public void run() {
    for (int i = 0; i < 20; i++) {
        System.out.println("新的线程执行"+i);
    }
}
}
   //主方法
  public static void main(String[] args) {
    RunnbaleImpl runnbale = new RunnbaleImpl(); //创建一个runnbale接口实现类对象
    Thread thread = new Thread(runnbale); //创建一个Thread类对象,构造方法中传递Runnable对象参数
    thread.start(); //执行Thread对象的start方法,开启多线程

    for (int i = 0; i < 20; i++) {
        System.out.println("主方法执行"+i);
    }
}
运行效果:
主方法执行0
新的线程执行0
主方法执行1
新的线程执行1
主方法执行2
新的线程执行2
主方法执行3
新的线程执行3
主方法执行4
新的线程执行4
主方法执行5
新的线程执行5
......

2.2.2Thread类和Runnbale接口两者的区别:

  • 避免了继承的局限性: Thread类是继承关系,在java中是单继承关系,一个类只能继承一个父类,这一点就有了局限性,而一个类可以实现多个接口,比较灵活,具有扩展性。
  • 降低了程序的耦合性: Thread类是线程任务的设置和开启线程任务放在一起,具有耦合性。而Runable接口只是负责重写了里面run线程任务方法,开启新的线程任务还是由Thread方法执行,起到了解耦的作用。
  • Runable接口适合多个线程共同访问共享一个资源。
  • 在线程池中只能放Thread类的线程,不能放Thread类

2.2.3 利用匿名内部类的方式创建创建线程

  • 什么是匿名内部类? 我的理解,匿名匿名就是没有名字的类,因为没有名字所以只能使用一次,作用就是简化代码的书写。
  • 匿名内部里类的使用条件:必须要继承一个父类或者是一个接口的实现类,而这一点线程就都沾上了,方式1,你得继承Thread类,方式2 你的是Runnable接口的实现类,匿名类大大简化线程代码的编写。

代码入下:

 public static void main(String[] args) {
    show01();
    show02();
}

private static void show02() {
   //也是利用匿名内部类的方式创建线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("我是实现的Runnable接口的线程"+Thread.currentThread().getName());
        }
    }).start();// 我这里简化写一起了,new Thread(xxx).start();
}

private static void show01() {
    //利用匿名内部类的方式创建线程Thread
    new Thread(){
        @Override
        public void run() {
            System.out.println("我是继承了Thread类的线程"+Thread.currentThread().getName());
        }
    }.start();
}

线程安全问题

由上面的继承Runnable接口我们得知可以访问并且共享同一个资源,但是在没有规定的情况下,多个线程访问资源的时肯能会产生线程问题。

问题:假如你要和你的女朋友去看哪吒(最近很火的)。

用美团订票的时候买的是7排的4,5号座位(我一般都是在后面的),但是你去了发现你的座位有人而且票的位置和场次都是和你一样的,这时候怎么版办?

什么意思呢? 就是三个顾客都通过手机去买共同的一百张电影票,由于共同访问共享资源然后就出现了线程安全问题。 代码如下:

int ticket = 100;//假如这场带电影做100个人
@Override
public void run() {
    //为了保证一直出票
    for (;;) {
        if(ticket > 0) {
            try {
                Thread.sleep(20);//网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在出第"+ticket+"张票");
            ticket --;//相应的票数减少
        }
    }
    
     public static void main(String[] args) {
    //创建线程的实现类
    TicketImpl ticket = new TicketImpl();
    //假如同时三个想买票
    Thread customer1 = new Thread(ticket);
    customer1.setName("1号顾客");
    Thread customer2 = new Thread(ticket);
    customer2.setName("2号顾客");
    Thread customer3 = new Thread(ticket);
    customer3.setName("3号顾客");
    customer1.start();
    customer2.start();
    customer3.start();
    }
    
    运行效果:
    2号顾客正在出第100张票
    1号顾客正在出第100张票
    3号顾客正在出第100张票
    2号顾客正在出第97张票
    1号顾客正在出第97张票
    3号顾客正在出第97张票
    3号顾客正在出第94张票
    .....
    3号顾客正在出第4张票
    1号顾客正在出第2张票
    3号顾客正在出第1张票
    2号顾客正在出第0张票
    1号顾客正在出第-1张票

不光出现了重复的票数,还出现了不存在的票数。 原因如下:

感觉写的有点长:分开写,预知后事如何,请看下回分解。

文章在写笔记的,(写的和我的日记差不多)中间查看了不少的博客以下是我引用的博客:

线程死掉之后的结果:

blog.csdn.net/javaer_lee/…

一个线程崩溃了,线程所在的进程是不是就要崩溃?

blog.csdn.net/mangobar/ar…