多线程,锁机制,乐观悲观,线程池、、、、

线程的理解

什么是进程?什么是线程?

进程大家可以理解为一个整体的框架类的线程,其中包含着很多的线程,就好像是在超市买东西一样,付款的时候,每次都会扫描商品的条形码,这个时候, 扫码的那个机器,就可以称之为一一个进程,而我们扫每个商品的条形码的时候,就是一个线程。在关闭扫码器之后,就代表着我们这个进程死亡了。所以大家可以将进程,理解为一些线程的合集。

多线程

java的特性中包含着多线程,大家要理解一个串行和并行,搞清楚这个,才能更好理解多线程。
串行:其实就是相对于单条线程来执行多个任务。我用进站口来给大家举个栗子,如果我们只有一个进站口的时候,大家是都需要排队的,一个一个进。
并行:现在有很多个进站口,为了提升效率,多个人可以从多个站口进入,在同一时刻发生,并行时间上是重叠的。

多线程:多线程其实就是,现在大家电脑上大多都带有安全管家一类的软件,这就是一个进程,里面有很多功能,有病毒查杀,快速体检等一些列功能。这时如果说,我们在执行某一个功能的时候,必须等待其执行完之后,才能执行别的功能,这就是单线程的理论。而当我们可以在执行某一功能的同时还能执行别的功能,这就是多线程 。而且是同时,严格意义上来说是同一时刻发生,且没有先后顺序。所以多线程其实就是把一个人任务分配给多个人同时执行,最后执行的效率会大大提高。

什么是线程安全 ?

既然是多线程,就肯定会有县城安全问题出现,所有的问题其实都是围绕着我们,多线程的每一条线程能否按照我们预期的设想去执行。

如何保证线程安全?(各种锁的应用)

Synchronized

Synchronized关键字,其主要目的就是保证线程的同步,保证我们在多线程的环境下,不被多个线程同时执行,确保我们数据的完整性,多加于方法之上。

public class SyncTest(){

	int count = 0;//被调用的次数。
	
	public syncronized void syncMethod(int j){
		count++;
		int i =1;
		j=j+1;
	}
	
}

Synchronized其实锁的不是方法本身, 而是方法后面括号里的那个对象,而不是代码。其次,对于非静态的Synchronized方法,锁的是对象本身也就是this。当Synchronized锁到对象的时候,别的线程执行时,获取这个对象就必须要等到,我们第一次请求的锁,归还之后,它才能执行。

缺点:虽然可以让我们的线程变得安全,但是总体是对某一个资源上锁,届时别的对象想拿到锁执行,你却一直占用到不释放,就有点浪费资源。

Lock

Lock是在java1.6之后被引入进来的,Lock的引入让锁有了可操作性。我们可以在 需要的时候去手动的获取锁或者释放锁,也可以中断获取,以及超时获取。就使用上来说Lock没有Synchronized方便。 这个咱们后面细讲。

实现方式

继承Thread类,重写run方法

public class ThreadDemo01 extends Thread{
    @Override
    public void run(){
        //编写自己的线程代码
        for (int i = 0; i <30; i++) {
            System.out.println(Thread.currentThread().getName()+"===我是run="+i);
        }
    }
}

class MainClass{
    public static void main(String[] args){
        for (int i = 0; i < 30; i++) {
            ThreadDemo01 t = new ThreadDemo01();
            t.setName("我是线程=="+i);
            t.start();
        }
    }
}

实现Runnable接口,重写run方法

public class ThreadDemo02 {

    public static void main(String[] args){ 
     
        for (int i = 0; i < 50; i++) {
            Thread t1 = new Thread(new MyThread());
            t1.start();
        }
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现接口的线程实现方式!");
    }   
}

通过实现Callable接口,实现call方法创建线程,和FutureTask创建线程

public class ThreadDemo04 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);

        Thread t = new Thread(oneTask);

        System.out.println(Thread.currentThread().getName());

        t.start();

    }

}

class Tickets<Object> implements Callable<Object>{

    //重写call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   
}

线程池实现

什么是线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,

还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线池,必须对其实现原理了如指掌。

线程池作用

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
线程池的分类

线程池的创建方式

线程池的五种创建方式

https://blog.csdn.net/u011479540/article/details/51867886

Java通过Executorsjdk1.5并发包)提供四种线程池,分别为:

newCachedThreadPool

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要可灵活回收空闲线程,若无可回收,则新建线程。
newCachedThreadPool
1、初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
2、和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过规定时间(keepAliveTime),会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
所以,使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。

public static void main(String[] args) {
    // 无限大小线程池 jvm自动回收
    ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int temp = i;
        newCachedThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // TODO: handle exception
                }
                System.out.println(Thread.currentThread().getName() + ",i:" + temp);

            }
        });
    }
}

总结: 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

newFixedThreadPool

newFixedThreadPool 创建一个定长线程池可控制线程最大并发数超出的线程会在队列中等待。

newFixedThreadPool
初始化一个指定线程数的线程池,其中corePoolSize == maximumPoolSize,使用LinkedBlockingQuene作为阻塞队列,不过当线程池没有可执行任务时,也不会释放线程,超出的线程会在队列中等待。示例代码如下:

public static void main(String[] args) {
    ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
        final int temp = i;
        newFixedThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + ",i:" + temp);

            }
        });
    }
}

创建了五个线程,你循环十次时会创建五个线程,那多余的会进行等待。
总结:
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

newSingleThreadExecutor

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newSingleThreadExecutor
初始化的线程池中只有一个线程如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行内部使用LinkedBlockingQueue作为阻塞队列

public static void main(String[] args) {
    ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        newSingleThreadExecutor.execute(new Runnable() {

            @Override
            public void run() {
                System.out.println("index:" + index);
                try {
                    Thread.sleep(200);
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
        });
    }
newSingleThreadExecutor.shutdown();
}

注意: 结果依次输出,相当于顺序执行各个任务。单线程执行,用的最少。

newScheduledThreadPool

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

public static void main(String[] args) {
    ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
    for (int i = 0; i < 10; i++) {
        final int temp = i;
        newScheduledThreadPool.schedule(new Runnable() {
            public void run() {
                System.out.println("i:" + temp);
            }
        }, 3, TimeUnit.SECONDS);
    }
}

表示延迟3秒才开始执行。
除了newScheduledThreadPool的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类实现的

ThreadPoolExecutor

ThreadPoolExecutor
JDK1.5后,有了很大的改善。JDK1.5之后加入了java.util.concurrent包,java.util.concurrent包的加入给予开发人员开发并发程序以及解决并发问题很大的帮助。
Executor框架的最顶层实现是ThreadPoolExecutor类Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池。

corePoolSize: 核心池的大小。 当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能创建多少个线程;
keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。
unit: 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

线程池原理剖析

https://blog.csdn.net/u010963948/article/details/80573898

提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果没有执行(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。
2、如果核心线程都在执行任务,线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里,核心线程有空闲时从工作队列拿任务开始执行。
3、如果工作队列满了,判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果最大线程池已经满了,则交给饱和策略执行。
线程池饱和策略
这里提到了线程池的饱和策略,那我们就简单介绍下有哪些饱和策略:
"
AbortPolicy
为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
DiscardPolicy
直接抛弃,任务不执行,空方法
DiscardOldestPolicy
从队列里面抛弃head的一个任务,并再次execute 此task。
CallerRunsPolicy
在调用execute的线程里面执行此command,会阻塞入口
用户自定义拒绝策略(最常用)
实现RejectedExecutionHandler,并自己定义策略模式
以下以ThreadPoolExecutor为例展示下线程池的工作流程图

合理配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
任务的优先级:高,中和低。
任务的执行时间:长,中和短。

某些进程花费了绝大多数时间在计算上,而其他则在等待I/O上花费了大多是时间,
前者称为计算密集型(CPU密集型)computer-bound,后者称为I/O密集型,I/O-bound。
CPU密集型指的是CPU在频繁调度中,这时任务可以少配置线程数,因为CPU密集时线程数可以复制,自己可以少创建线程,使用CPU线程,创建的线程数大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
IO密集型时,大部分线程都会阻塞,线程不容易复用,故需要多配置线程数,2*cpu核数

多线程的常用方法


run()和start()

这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中start()方法启动线程将自动调用 run()方法,这是由Java的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void.

sleep()

使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不同访问共享数据注意该方法要捕获异常

比如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。

总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

join()

join()方法使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行。注意该方法也要捕获异常

yield()

它与sleep()类似只是不能由用户指定暂停多长时间并且yield()方法只能让同优先级的线程有执行的机会

wait()和notify()、notifyAll()

这三个方法用于协调多个线程对共享数据的存取所以必须在Synchronized语句块内使用这三个方法。前面说过Synchronized 这个关键字用于保护共享数据,阻止其他线程对共享数据的存取。但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出Synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。

wait()方法使当前线程暂停执行并释放对象锁标志,让其他线程可以进入Synchronized数据块,当前线程被放入对象等待池中。当调用 notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有

锁标志等待池中的线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。

notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。

多线程的三大特性

原子性、可见性、有序性

原子性:

线程的一个或者多个操作要么全部执行,而且执行过程不会被打断,要么全部都不执行。

可见性:

可见性是指多个线程访问同一个变量的时候,一个线程修改了这个变量的值其他线程也可以立刻看到这个修改后的值Java提供关键字volatile关键字来保证可见性当一个共享变量被volatile修饰后,它会保证修改的值立即更新到主存中,其他线程如果需要用到这个变量,会到主存中去取,这样保证每个线程读取到的数据都是最新的)。Synchronized和Lock也能保证可见性,Synchronized和Lock保证同一时刻只有一个线程能够获得锁然后执行同步代码,并且在释放锁之前,将变量的值刷新到主存中,因此可以保证可见性。

使用volatile关键字后,才能保证可见性,不使用是没有可见性的,就如同购票流程,加了锁才能保证可见性。

有序性:

即程序的执行顺序按照代码的先后顺序执行。(在java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响单线程程序的执行,但是会影响多线程并发执行的正确性)

java内存模型

java内存模型是专门针对多线程数据同步使用。
它分为主内存,存放的是初始化全局变量;
本地内存是多线程使用的内存,比如A线程启动后,先从主线程中拿到变量的副本,放入到本地内存中,当线程把全局变量进行更改后才开始更新主内存数据,当线程没有更改全局变量的时候,线程就会一直拿本地线程去使用,这时候就会造成一个问题,B线程更改了本地内存数据,于是B线程把更改完的数据同步到了主内存当中,由于A线程并没有更改本地副本数据,所以A线程还会一直拿本地副本数据区执行,所以就会造成数据不一致问题。为了解决这个问题,使用volatile关键字,当B线程进行更改后,会通知主线程,主线程会通知各个线程更新本地数据。

什么是Volatile

Volatile 关键字的作用是变量在多个线程之间可见。
下面先演示线程数据不可见的问题:

public class ThreadVolatileDemo extends Thread {

   public boolean flag = true;
   @Override
   public void run() {
      System.out.println("开始执行子线程....");
      while (flag) {
      }
      System.out.println("线程停止");
   }
   public void setRuning(boolean flag) {
      this.flag = flag;
   }
}

class ThreadVolatile {
   public static void main(String[] args) throws InterruptedException {
      ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
      threadVolatileDemo.start();
      Thread.sleep(3000);
      threadVolatileDemo.setRuning(false);
      System.out.println("flag 已经设置成false");
      Thread.sleep(1000);
      System.out.println(threadVolatileDemo.flag);
   }
}

这个关键字的作用是让本地内存变化后强制去刷新主内存。它虽然保证了可见性,但无法保证原子性,下面就是原子性失效的例子。

public class VolatileNoAtomic extends Thread {
   private static volatile int count;

   // private static AtomicInteger count = new AtomicInteger(0);
   private static void addCount() {
      for (int i = 0; i < 1000; i++) {
         count++;
         // count.incrementAndGet();
      }
      System.out.println(count);
//System.out.println(count.get());
   }

   public void run() {
      addCount();
   }

   public static void main(String[] args) {

      VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
      for (int i = 0; i < 10; i++) {
         arr[i] = new VolatileNoAtomic();
      }

      for (int i = 0; i < 10; i++) {
         arr[i].start();
      }
   }

}

通过jdk1.5之后并发包里的原子类AtomicInteger来解决数据不一致问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值