大数据之spark_spark中的线程安全问题

Task多线程问题

问题原因

一个Executor是一个进程,一个进程中可以同时运行多个线程Task,如果多个Task使用了共享的成员变量,就会出现线程不安全的问题

案例

需求
使用spark将日期字符串转换成long类型时间戳

样例数据
2019-11-06 15:59:50
2019-11-06 15:59:51
2019-11-06 15:59:52
2019-11-06 15:59:53
2019-11-06 15:59:54
2019-11-06 15:59:55
2019-11-06 15:59:56
2019-11-06 15:59:57
2019-11-06 15:59:58

错误示范:

将new SimpleDateFormat定义为成员对象,并使用object修饰的工具类,就会存在线程安全问题

package com.doit.spark.day08

import java.text.SimpleDateFormat

object DateUtils2 {
  
  //将new SimpleDateFormat定义为成员对象,并使用object修饰的工具类,就会存在线程安全问题
  val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

  def dataToLong (dateU: String) ={

    val time: Long = sdf.parse(dateU).getTime

    time

  }
}

package com.doit.spark.day08

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object DateDemo2 {
  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf().setAppName("DateDemo")

    val setMaster = args(0).toBoolean

    if (setMaster){
      conf.setMaster("local[*]")
    }
    val sc = new SparkContext(conf)
    val textFile = args(1)
    val line: RDD[String] = sc.textFile(textFile)

    val maped: RDD[Long] = line.map(x => {
      
      val timeLong: Long = DateUtils2.dataToLong(x)
      timeLong
    })

    println(maped.collect().toBuffer)

  }
}

报如下错误(几率性的),因为多个Task去抢一个Executor中的SimpleDateFormat对象,该对象需要对时间进行格式转换,可能一个Task转换到一半的时候,另一个Task把进程抢了过去,就会报NumberFormatException的错误

在这里插入图片描述

解决方式一:

将new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)写在局部变量的位置上,但是这样每来一条数据就会在该方法中new一个对象,浪费大量的内存空间,不推荐

package com.doit.spark.day08

import java.text.SimpleDateFormat

object DateUtils2 {
  
  def dataToLong (dateU: String) ={

    val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

    val time: Long = sdf.parse(dateU).getTime

    time

  }
}

解决方式二:

给dataToLong方法加锁synchronized,但这样会使程序的执行速度变慢,因为这样相当于把多线程变成了单线程

package com.doit.spark.day08

import java.text.SimpleDateFormat

object DateUtils2 {

  val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  //给方法加锁
  def dataToLong (dateU: String) = synchronized{

    val time: Long = sdf.parse(dateU).getTime
    time
  }
}

解决方式三:

不使用SimpleDateFormat转换时间格式,因为它没有线程锁,可以使用另一个线程安全的工具类FastDateFormat,它里面有线程锁,自动保证线程安全,但是依然会降低效率,注意导包时导lang3的包

package com.doit.spark.day08

import java.text.SimpleDateFormat

import org.apache.commons.lang3.time.FastDateFormat

object DateUtils2 {

  //val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

  private val dateFormat: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
  def dataToLong (dateU: String) = synchronized{

    val time: Long = dateFormat.parse(dateU).getTime
    time
  }
}

解决方式四:

定义工具类时,使用class对象,然后在Driver端new工具类的对象,使用闭包的形式传入到函数中,如果对象中的信息量很大,就会在序列化后传输后,然后在多个Executor反序列化浪费大量的资源和时间

package com.doit.spark.day08

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object DateDemo {
  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf().setAppName("DateDemo")
    val setMaster = args(0).toBoolean
    if (setMaster){
      conf.setMaster("local[*]")
    }
    val sc = new SparkContext(conf)
    val textFile = args(1)
    val line: RDD[String] = sc.textFile(textFile)

    //new在Driver端
    val dateUtils = new DateUtils
    
    val maped: RDD[Long] = line.mapPartitions(it => {
      it.map(x => {
      val timeLong: Long = dateUtils.dataToLong(x)
      timeLong
      })
    })
    println(maped.collect().toBuffer)
  }
}

解决方式五:

使用mapPartitions算子,然后在该算子内new工具类,这样即不会浪费大量的内存,也不会存在线程安全问题

package com.doit.spark.day08

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object DateDemo {
  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf().setAppName("DateDemo")

    val setMaster = args(0).toBoolean

    if (setMaster){
      conf.setMaster("local[*]")
    }
    val sc = new SparkContext(conf)
    val textFile = args(1)
    val line: RDD[String] = sc.textFile(textFile)

    
	//使用mapPartitions算子,每个分区new一个对象,该分区内的数据,共享该对象
	//相当于一个Task用一个对象,所以不会存在线程安全问题,同时节省了内存空间
    val maped: RDD[Long] = line.mapPartitions(it => {
      //new在Executor端
      val dateUtils = new DateUtils
      it.map(x => {
      val timeLong: Long = dateUtils.dataToLong(x)
      timeLong
      })
    })
    println(maped.collect().toBuffer)
  }
}

案例:广播会变的ip规则数据

使用普通的广播变量广播之后,规则数据就不能再改变,所以,我们不能用广播变量的形式,将数据广播出去,我们可以使用object定义的单例对象,然后在该对象中定义一个ArrayBuffer储存规则数据,再在RDD函数中调用该对象,获取规则数据,在该函数被调用时(处理第一条数据时),就会初始化该对象,获取规则数据,然后所有的数据都可以使用该对象进行规则匹配
然后我们可以设置一个定时器,定时从某个文件中更新ArrayBuffer中的规则数据,更新后发送到RDD函数中,但需要加一个读写锁(ReentrantReadWriteLock),避免读写同时进行,出现线程安全问题.

代码实现

package com.doit.spark.day08




import java.io.{BufferedReader, FileInputStream, InputStreamReader}

import scala.collection.mutable.ArrayBuffer

object IpRulesLoader {

  //定义一个可变数组ArrayBuffer,用来装规则数据
  //在object中定义的定义的数据是静态的,在一个JVM进程中,只有一份
  var ipRules = new ArrayBuffer[(Long, Long, String, String)]()
  //加载IP规则数据,在Executor的类加载是执行一次
  //静态代码块
  //读取HDFS中的数据
  //val fileSystem = FileSystem.get(URI.create("file://"), new Configuration())
  //val inputStream = fileSystem.open(new Path("/Users/xing/Desktop/ip.txt"))

  //读取本地的数据
  private val bf = new BufferedReader(new InputStreamReader(new FileInputStream("文件路径")))

  var line:String = null
  //此处必须使用do  while循环,因为它要循环的将文件中所有数据都读取到数组中,且必须至少先读取一次
  do {
    line = bf.readLine()
  if (line != null){
    //处理IP规则数据
    val fields = line.split("[|]")
    val startNum = fields(2).toLong
    val endNum = fields(3).toLong
    val province = fields(6)
    val city = fields(7)
    //放入元组,然后放入数组中
    val t = (startNum, endNum, province, city)
    ipRules += t

  }
  }while(line != null)
  //调用该方法时.返回规则数组
  def getAllRules(): ArrayBuffer[(Long, Long, String, String)] = {
    ipRules
  }
}

package com.doit.spark.day08

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ArrayBuffer

object IpLocationV1 {
  def main(args: Array[String]): Unit = {
    val isLocal = args(0).toBoolean
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    if(isLocal) {
      conf.setMaster("local[*]")
    }
    val sc = new SparkContext(conf)

    //指定未来读取日志数据的位置
    val lines = sc.textFile(args(1))

    val value: RDD[String] = lines.map(line => {
      val fields = line.split("[|]")
      val ip = fields(1)
      //将IP地址转成10进制
      val ipNumber: Long = IpUtils.ip2Long(ip)
      //根据ipNumber查找规则,返回省市
      val rules: ArrayBuffer[(Long, Long, String, String)] = IpRulesLoader.ipRules
      val index: Int = IpUtils.binarySearch(rules, ipNumber)
      var province = "未知"
      if (index != -1) {
        province = rules(index)._3
      }
      province
    })
    println(value.collect().toBuffer)
  }
}

package com.doit.spark.day08

import scala.collection.mutable.ArrayBuffer

object IpUtils {

    /**
     * 将IP地址转成十进制
     *
     * @param ip
     * @return
     */
    def ip2Long(ip: String): Long = {
      val fragments = ip.split("[.]")
      var ipNum = 0L
      for (i <- 0 until fragments.length) {
        ipNum = fragments(i).toLong | ipNum << 8L
      }
      ipNum
    }

    /**
     * 二分法查找
     *
     * @param lines
     * @param ip
     * @return
     */
    def binarySearch(lines: ArrayBuffer[(Long, Long, String, String)], ip: Long): Int = {
      var low = 0 //起始
      var high = lines.length - 1 //结束
      while (low <= high) {
        val middle = (low + high) / 2
        if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
          return middle
        if (ip < lines(middle)._1)
          high = middle - 1
        else {
          low = middle + 1
        }
      }
      -1 //没有找到
    }

    def binarySearch(lines: Array[(Long, Long, String, String)], ip: Long): Int = {
      var low = 0 //起始
      var high = lines.length - 1 //结束
      while (low <= high) {
        val middle = (low + high) / 2
        if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
          return middle
        if (ip < lines(middle)._1)
          high = middle - 1
        else {
          low = middle + 1
        }
      }
      -1 //没有找到
    }
  }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值