java并发编程 线程基础/线程之间的共享和协作

优采云 发布时间: 2020-04-05 11:36

  

  一言以蔽之:Java 并发编程的目的就是充分利用计算机的资源,把计算机的性能发挥到最大

  

  什么是高并发

  并发 concurrency 和并行 parallelism 的区别

  并发是指多个线程操作同一个资源,不是同时操作,而是交替操作,单核 CPU,只不过由于速率很快,看起来是同时执行(张三、李四,共用一口锅煮饭,交替执行),通过时间片轮转机制RR调度实现并发。

  速度很快,看起来象是并行。

  并行才是真正的同时执行,多核 CPU,每个线程使用一个独立的 CPU 的资源来运行。(张三、李四,一人一口锅,一起炖肉)

  并发编程是指使系统容许多个任务在重叠的时间段内执行的设计结构。

  高并发我们设计的程序,可以支持海量任务的同时执行(任务的执行在时间段上有重叠的情况)

  QPS:每秒响应的恳求数,QPS 并不是并发数。

  吞吐量:单位时间内处理的恳求数,QPS 和并发数决定。

  平均响应时间:系统对一个恳求做出响应的平均时间,QPS = 并发数/平均响应时间

  并发用户数:系统可以承载的最大用户数目。

  高并发编程优劣

  利:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化

  弊:线程共享资源,存在冲突。容易造成死锁。 启用太多的线程,就有扳倒机器的可能。

  互联网项目构架中,如何提升系统的并发能力?

  垂直扩充

  水平扩充

  垂直扩充

  提升单机的处理能力

  1、增强单机的硬件性能:增加 CPU 的核数,硬盘扩容,内存升级。

  2、提升系统的构架性能:使用 Cache 来提升效率,异步恳求降低单个服务的吞吐量,使用 NoSQL 来提高数据的访问性能。

  水平扩充

  集群、分布式都是水平扩充的方案

  集群:多个人做同一件事情(3 个面点师同时炖肉)

  分布式:把一件复杂的事情,拆分成几个简单的步骤,分别找不同的人去完成 (1 洗菜、2 切菜、3 炒菜)

  

  

  分布式是指多个系统协同合作完成一个特定任务的系统。分布式解决问题,把所有解决中心化管理的任务叠加到一个节点处理,太慢了。把一个业务分拆为多个子业务(当某个子业务访问量突增时,增加该子任务的节点数即可),部署在多个服务器上 ,分布式的主要工作是分解任务,将职能拆解。

  集群主要的使用场景是为了分担恳求的压力,同一个业务,部署在多个服务器上 ,也就是在几个服务器上布署来==相同的应用程序,==分担客户端恳求。当压力进一步减小的时侯,可能在须要储存的部份,mysql 无法面对好多的写压力。因为在 mysql 做成集群以后,主要的写压力还是在 master 的机器里面,其他 slave 机器难以分担写压力,从而这个时侯,也就引下来分布式。分布式的主要应用场景是单台机器早已未能满足这些性能的要求,必须要融合多个节点,并且节点之间是相关之间有交互的。相当于在写 mysql 的时侯,每个节点储存部份数据,也就是分布式存储的来历。存储一些非结构化数据:静态文件、图片、pdf、小视频 … 这些也就是分布式文件系统的来历。

  集群主要是。分布式中的某个子任简单加机器解决问题,对于问题本身不做任何分解;分布式处理里必然包含任务分解与答案归并务节点,可能由一个集群来取代;集群中任一节点,都是做一个完整的任务。集群和分布式都是由多个节点组成,。但是集群之间的通讯协调基本不需要;而分布式各个节点的通讯协调必不可少RPC

  将一套系统分拆成不同子系统布署在不同服务器上(这叫分布式),

  然后布署多个相同的子系统在不同的服务器上(这叫集群),部署在不同服务器上的同一个子系统应做负载均衡。

  负载均衡集群:一个集群节点来接受任务,然后按照其余节点的工作状态(CPU,内存等信息)派发任务量, 最终实现完成任务。

  SOA:业务系统分解为多个组件,让每位组件都独立提供离散,自治,可复用的服务能力,通过服务的组合和编排来实现下层的业务流程

  作用:简化维护,降低整体风险,伸缩灵活

  微服务:架构设计概念,各服务间隔离(分布式也是隔离),自治(分布式依赖整体组合)其它特点(单一职责,边界,异步通信,独立布署)是分布式概念的跟严格执行SOA到微服务构架的演化过程 作用:各服务可独立应用,组合服务也可系统应用。SpringClond就是太精典的微服务框架。

  具体的推行方案

  1.站点层扩充:Nginx 方向代理,高并发系统,一个 Tomcat 带不上去,就找十个 Tomcat 去带

  

  2.服务层扩充:通过 RPC 框架实现远程调用,Dubbo、Spring Boot/Spring Cloud,将业务逻辑分拆到不同的 RPC Client,各自完成不同的业务,如果个别业务的并发量很大,就降低新的 RPC Client,理论上实现无限高并发。

  3.数据层扩充:一台数据库拆成多台,主从复制、读写分离、分表分库。

  ** 进程和线程**

  进程就是计算机正在运行的一个独立的应用程序,进程是一个动态的概念,必须是运行状态,如果一个应用程序没有启动,那就不是进程。

  线程就是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成。

  进程和线程的区别在于运行时是否拥有独立的显存空间,每个进程所拥有的空间都是独立的,互不干扰,多个线程是共享内存空间的,但是每位线程的执行是互相独立。

  线程必须依赖于进程能够执行,单独线程是难以执行的,由进程来控制多个线程的执行。

  Java 默认有几个线程?

public class OnlyMain {

public static void main(String args) {

//虚拟机线程管理的接口

ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();

ThreadInfo threadInfos =

threadMXBean.dumpAllThreads(false, false);

for(ThreadInfo threadInfo:threadInfos) {

System.out.println("["+threadInfo.getThreadId()+"]"+" "

+threadInfo.getThreadName());

}

}

}

  Java 默认有两个线程:Main 和 GC

  多线程实现方法

  继承 Thread

  实现 Runnable

  实现 Callable

  重点:Thread 是线程对象,Runnable 是任务,一线程启动的时侯一定是对象。

  

```java

package com.southwind.demo;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.FutureTask;

public class Test {

public static void main(String args) {

MyCallable myCallable = new MyCallable();

FutureTask futureTask = new FutureTask(myCallable);

Thread thread = new Thread(futureTask);

thread.start();

//获取Callable的返回值

try {

System.out.println(futureTask.get());

} catch (InterruptedException e) {

e.printStackTrace();

} catch (ExecutionException e) {

e.printStackTrace();

}

}

}

class MyCallable implements Callable<String>{

@Override

public String call() throws Exception {

System.out.println("callable");

return "hello";

}

}

  Runnable对象是Thread构造函数的参数,而Callable对象是FutureTask构造函数的参数,利用FutureTask这个对象,构建出最后的线程Thread。

  Runable和Thread承继以后的时侯,都须要重画run技巧。而Callable对象,重写的是call方式。

  Callable 与 Runnable 的区别:

  Callable 的 call 方法有返回值,Runnable 的 run 方法没有返回值。

  Callable 的 call 方法可以抛出异常,Runnable 的 run 方法不能抛出异常。

  在外部通过 FutureTask 的 get 方法异步获取执行结果,FutureTask 是一个可以控制的异步任务,是对 Runnable 实现的一种承继和扩充。

  get 方法可能会形成阻塞,一般置于代码的最后。Callable 有缓存。

  线程状态

  

  线程只有6种状态。整个生命周期就是这几种状态的切换。

  run()和start() :run方式就是普通对象的普通方式,只有调用了start()后,Java就会将线程对象和操作系统中实际的线程进行映射,再来执行run技巧。

  yield() :Thread.yield(),让出cpu的执行权,将线程从运行转入可运行状态,但是下个时间片,该线程仍然有可能被再度选中运行 。

  线程的优先级 取值为1~10,缺省为5,最高10最低1,但线程的优先级不可靠,只是一个大几率执行而已,不建议作为线程开发时侯

  守护线程:和主线程共死,finally不能保证一定执行

  synchronized 用法

  修饰实例方式,对当前实例对象this加锁

public class SynchronizedDemo {

public synchronized void methodOne() {

.....

}

}

  修饰静态方式,对当前类的Class对象加锁

public class SynchronizedDemo {

public static synchronized void methodTwo() {

......

}

}

  修饰代码块,指定加锁对象,对给定对象加锁

public class SynchronizedDemo {

public void methodThree() {

// 对当前实例对象this加锁

synchronized (this) {

}

}

public void methodFour() {

// 对class对象加锁

synchronized (SynchronizedDemo.class) {

....

}

}

}

  volatile

  volatile适合于只有一个线程写,多个线程读的场景,因为它只能确保可见性。

  java编程语言容许线程访问共享变量,为了确保共享变量能被确切和一致的更新,线程应当确保通过排他锁单独获得这个变量。Java语言提供了volatile,在个别情况下比锁愈加便捷。如果一个数组被申明成volatile,java线程显存模型确保所有线程听到这个变量的值是一致的。

  这句话的涵义有两层.

  volatile 的写操作, 需要将线程本地显存值,立马刷新到 主显存的共享变量中.

  volatile 的读操作, 需要从主显存的共享变量中读取,更新本地内存变量的值.

  由此引出 volatile 的显存语义.

  当写一个volatile变量时,JMM会把该线程对应的本地显存中的共享变量值刷新到主显存.

  当读一个volatile变量时,JMM会把该线程对应的本地显存置为无效。线程接下来将从主显存中读取共享变量,并更新本地显存的值.

  PS :本来在JVM中有共享变量县以及技巧的栈县,栈区里有共享县变量的高速缓存的,一般方式县直接用高速缓存县数据来运算。

  volatile 的特点

  可见性 : 对一个volatile的变量的读,总是能看到任意线程对这个变量最后的写入

  互斥性 : 同一时刻只容许一个线程对变量进行操作.(互斥锁的特性)

  不具有原子性:复合操作不具有(如inc++ 等价inc= inc+ 1)

  敲黑板,划重点,volatitle不具有原子性的重点

  假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行更改操作,所以不会造成线程2的工作显存中缓存变量inc的缓存行无效,也不会造成寻址中的值刷新,所以线程2会直接去寻址读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作显存,最后写入寻址。

  然后线程1接着进行加1操作,由于早已读取了inc的值(inc++,包括3个操作python 多线程网络爬虫,1.读取inc的值,2.进行加1操作,3.写入新的值),注意此时在线程1的工作显存中inc的值依旧为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作显存,最后写入寻址。

  那么两个线程分别进行了一次自增操作后,inc只降低了1。根源就在这里,自增操作不是原子性操作,而且volatile也难以保证对变量的任何操作都是原子性的。

  解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过使用AtomicInteger

  什么是JMM

  JMM(java memory model)决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的具象关系:共享变量储存在主显存(Main Memory)中,每个线程都有一个私有的本地显存(Local Memory),本地显存保存了被该线程使用到的主显存的副本拷贝,线程对变量的所有操作都必须在工作显存中进行,而不能直接读写主显存中的变量。

  

  声明变量为volatile后 方法县中的数据本地缓存就失效了,每次都要在主显存中拿。

  ThreadLocal

  线程局部变量。进来类型简单点哦,可以理解为是个map,比如类型 Map<Thread,Integer>python 多线程网络爬虫,跟Python中的线程局部变量类似。

public class UseThreadLocal {

//可以理解为 一个map,类型 Map<Thread,Integer>

static ThreadLocal<Integer> threadLaocl = new ThreadLocal<Integer>() {

@Override

protected Integer initialValue() {

return 1;

}

};

/**

* 运行3个线程

*/

public void StartThreadArray() {

Thread runs = new Thread[3];

for (int i = 0; i < runs.length; i++) {

runs = new Thread(new TestThread(i));

}

for (int i = 0; i < runs.length; i++) {

runs[i].start();

}

}

/**

* 类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响

*/

public static class TestThread implements Runnable {

int id;

public TestThread(int id) {

this.id = id;

}

public void run() {

System.out.println(Thread.currentThread().getName() + ":start");

Integer s = threadLaocl.get();//获得变量的值

s = s + id;

threadLaocl.set(s);

System.out.println(Thread.currentThread().getName() + ":" + threadLaocl.get());

//threadLaocl.remove();

}

}

public static void main(String args) {

UseThreadLocal test = new UseThreadLocal();

test.StartThreadArray();

}

}

  wait notify

  wait是指在一个早已步入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方式(notify并不释放锁,只是告诉调用过wait方式的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在他人手里,别人还没释放),调用wait方式的一个或多个线程都会解除wait状态,重新参与竞争对象锁,程序假如可以再度得到锁,就可以继续向上运行。

  wait()、notify()和notifyAll()方法是本地方式,并且为final方式,无法被重画。

  当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能使当前线程阻塞。(这种阻塞是通过提早释放synchronized锁,重新去恳求锁造成的阻塞,这种恳求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)

  调用某个对象的notify()方法才能唤起一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤起其中一个线程; (notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方式或则语法块执行完才真正释放锁)

  调用notifyAll()方法才能唤起所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的机率是随机的,取决于cpu调度

  调用yield() 、sleep()、wait()、notify()等方式对锁有何影响?

  线程在执行yield()以后,持有的锁是不释放的,操作系统一直可能选择到该进程。不释放锁

  sleep()方法被调用之后,持有的锁是不释放的,休眠期间操作系统不会再执行该进程。不释放锁

  调动方式之前,必须要持有锁。调用了wait()方法之后,锁都会被释放,当wait方式返回的时侯,线程会重新持有锁 .

  wait方式是面对对象的,表示当前的对象会步入阻塞,在notify/all调用以后,会重新步入就绪状态。

  调动方式之前,必须要持有锁,调用notify()方法本身不会释放锁的,只是告诉调用过wait方式的线程可以去参与获得锁的竞争了.[/i]

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线